Java classes

Content:

  1. Introduction
  2. Top level classes
  3. Static nested classes
  4. Inner classes
  5. Anonymous inner classes
  6. Method local inner classes
  7. enum (Java 5+)
  8. record (Java 14/16/17+)

Introduction:

All Java programmers use classes. One need to write a class to write hello world in Java.

But not all Java programmers understand the different types of classes in Java.

Java got:

Note that this is not 7 types of classes - this is two dimensions:

so in total Java got 15 types of classes.

This article will take a deeper dive into these.

Very basic stuff. And all examples will be super simple.

Top level classes:

Top level classes are as the name indicates classes that are not inside any other class.

This is the first type of class a begginer Java programmer learns. Very likely there are also some intermediate Java programmers that have only used this type of class.

Example:

package classes.singletoplevel;

public class Demo {
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
    }
}

There is a myth that there can only be one top level class per sourec file. That is not correct, but there can only be one public top level class per source file.

...
public class SomeClass {
    ...
}

class SomeOtherClass {
    ...
}

Example with two top level classes:

package classes.multipletoplevel;

public class Demo {
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void test() {
        Other other = new Other();
        other.setX(456);
        System.out.printf("Other x = %d\n", other.getX());
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
        demo.test();
    }
}

class Other {
    private int x;
    public int getX() {
        return x;
    }
    public void setX(int x) {
        this.x = x;
    }
}

20 years ago I frequently had additional package visible classes in my Java source files. Today I never do so - I prefer to use static nested classes. See next section.

Static nested classes:

A class can be declared inside another class.

...
public class OuterClass {
    ...
    public static class NestedClass {
        ...
    }
    ...
}
...

Inside OuterClass one can just use NestedClass. Outside OuterClass one need to use OuterClass.NestedClass as name.

Example:

package classes.staticnested;

public class Demo {
    public static class Other {
        private int x;
        public int getX() {
            return x;
        }
        public void setX(int x) {
            this.x = x;
        }
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void test() {
        Other other = new Other();
        other.setX(456);
        System.out.printf("Other x = %d\n", other.getX());
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
        demo.test();
    }
}

Note that Java static class and C# static class are totally different. A Java static class is just a nested class. A C# static class is a class that can only have static members.

Inner classes:

Without the static keyword the class becomes an inner class.

...
public class OuterClass {
    ...
    public class InnerClass {
        ...
    }
    ...
}
...

At first glance an inner class looks very similar to a static nested class.

Same example:

package classes.inner;

public class Demo {
    public class Other {
        private int x;
        public int getX() {
            return x;
        }
        public void setX(int x) {
            this.x = x;
        }
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void test() {
        Other other = new Other();
        other.setX(456);
        System.out.printf("Other x = %d\n", other.getX());
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
        demo.test();
    }
}

But the same rules applies to classes as to methods and field!

static
belongs to class
non static
belongs to instance

An inner class belongs to an outer instance not to the outer class.

Or to reword: an instance of an inner class has an implicit reference to the instance of the outer class it belongs to.

Let us modify the example to illustrate that:

package classes.inner.real;

public class Demo {
    public class Other {
        private int x;
        public int getX() {
            return x;
        }
        public void setX(int x) {
            this.x = x;
        }
        public void test() {
            System.out.printf("Outer v = %d\n",  getV());
        }
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void test() {
        Other other = new Other();
        other.setX(456);
        System.out.printf("Other x = %d\n", other.getX());
        other.test();
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
        demo.test();
    }
}

The method Other.test can call the method Demo.getV because Other is an inner class of Demo.

It could not have done that if Other has been a static nested class.

But inner classes can get quickly become complicated.

All Java programmers know the this keyword. But not all Java programmer know it is possible to prefix this with a class name to resolve name conflicts between inner and outer classes.

Example with 3 classes:

package classes.inner.bad1;

public class Demo {
    public class Other {
        public class YetAnother {
            private int v;
            public int getV() {
                return v;
            }
            public void setV(int v) {
                this.v = v;
            }
            public void test() {
                System.out.printf("v = %d\n",  getV());
                System.out.printf("this v = %d\n",  this.getV());
                System.out.printf("YetAnother this v = %d\n",  YetAnother.this.getV());
                System.out.printf("Other this v = %d\n",  Other.this.getV());
                System.out.printf("Demo this v = %d\n",  Demo.this.getV());
            }
        }
        private int v;
        public int getV() {
            return v;
        }
        public void setV(int v) {
            this.v = v;
        }
        public void test() {
            YetAnother yao = new YetAnother();
            yao.setV(789);
            yao.test();
        }
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void test() {
        Other other = new Other();
        other.setV(456);
        other.test();
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        demo.test();
    }
}

In this example with 3 classes that all has a V property then inside YetAnother: getV(), this.getV() and YetAnother.this.getV() refer to YetAnother V, Other.this.getV() refer to Other V and Demo.this.getV() refer to Demo V.

When using new InnerClass inside OuterClass, then the new instance of InnerClass belongs/refers to the creating instance of OuterClass. That is quite natural.

But what about outside OuterClass? Then it is necessary to prefix new with an instance of OuterClass.

Example:

package classes.inner.bad2;

public class Demo {
    public class Other {
        private int x;
        public int getX() {
            return x;
        }
        public void setX(int x) {
            this.x = x;
        }
        public void test() {
            System.out.printf("Outer v = %d\n",  getV());
        }
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
        Other other = demo.new Other();
        other.setX(456);
        System.out.printf("Other x = %d\n", other.getX());
        other.test();
    }
}

I try to avoid inner classes if possible and use static nested classes. I don't want the extra functionality/complexity unless I really need it.

Anonymous inner classes:

It is possible to create an instance of an inner class with no name. All one need is either a class that it extends or an interface that it implements.

All classes extends java.lang.Object, but an anonymous inner class is most useful if it extends something with specific methods.

new BaseType() {
    ...
}

Example with both interface and abstract class:

package classes.anonymousinner;

public class Demo {
    public static interface I {
        public int getX();
        public void setX(int x);
    }
    public static abstract class AC {
        public abstract int getY();
        public abstract void setY(int y);
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void testi() {
        I io = new I() {
            private int x;
            @Override
            public int getX() {
                return x;
            }
            @Override
            public void setX(int x) {
                this.x = x;
            }
        };
        io.setX(456);
        System.out.printf("I x = %d\n", io.getX());
    }
    public void testac() {
        AC aco = new AC() {
            private int y;
            @Override
            public int getY() {
                return y;
            }
            @Override
            public void setY(int y) {
                this.y = y;
            }
        };
        aco.setY(789);
        System.out.printf("AC y = %d\n", aco.getY());
    }
    public static void main(String[] args) {
        Demo o = new Demo();
        o.setV(123);
        System.out.printf("Demo v = %d\n", o.getV());
        o.testi();
        o.testac();
    }
}

And because it is an inner class then it belongs/refers to an instance of outer class.

Example:

package classes.anonymousinner.real;

public class Demo {
    public static interface I {
        public int getX();
        public void setX(int x);
        public void test();
    }
    public static abstract class AC {
        public abstract int getY();
        public abstract void setY(int y);
        public abstract void test();
    }
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void testi() {
        I io = new I() {
            private int x;
            @Override
            public int getX() {
                return x;
            }
            @Override
            public void setX(int x) {
                this.x = x;
            }
            @Override
            public void test() {
                System.out.printf("Outer v = %d\n",  getV());
            }
        };
        io.setX(456);
        System.out.printf("I x = %d\n", io.getX());
        io.test();
    }
    public void testac() {
        AC aco = new AC() {
            private int y;
            @Override
            public int getY() {
                return y;
            }
            @Override
            public void setY(int y) {
                this.y = y;
            }
            @Override
            public void test() {
                System.out.printf("Outer v = %d\n",  getV());
            }
        };
        aco.setY(789);
        System.out.printf("AC y = %d\n", aco.getY());
        aco.test();
    }
    public static void main(String[] args) {
        Demo o = new Demo();
        o.setV(123);
        System.out.printf("Demo v = %d\n", o.getV());
        o.testi();
        o.testac();
    }
}

At first glance this feature looks totally wierd. Who would want to do such a thing. But then one realize that an anonymous inner class is really a bundle of lambda expressions and then it makes sense. And in fact they are used. They are very common in Swing applications.

Not quite as common in Java 8+, because the introductions of actual lambda methods made all anonymous inner classes with just one method obsolete.

Example of pre Java 8 Swing code:

package classes.swing;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

public class OldStyle extends JFrame {
    private static final long serialVersionUID = 1L;
    public OldStyle() {
        setTitle("Swing class demo");
        addWindowListener(new WindowAdapter() { // anonymous inner class extending WindowAdapter
            public void windowClosing(WindowEvent e) {
                JOptionPane.showMessageDialog(OldStyle.this, "We are exiting"); // reference to outer class instance
            }
        });
        getContentPane().setLayout(new BorderLayout());
        JButton btn = new JButton("Click here");
        btn.addActionListener(new ActionListener() { // anonymous inner class implementing ActionListener
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(OldStyle.this, "You clicked"); // reference to outer class instance
            }
        });
        getContentPane().add(btn, BorderLayout.CENTER);
        pack();
    }
    public static void main(String[] args) {
       SwingUtilities.invokeLater(new Runnable() { // anonymous inner class implementing Runnable
            public void run() {
                JFrame f = new OldStyle();
                f.setVisible(true);
            }
        });
    }
}

Same code in Java 8+ using lambdas where possible:

package classes.swing;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

public class NewStyle extends JFrame {
    private static final long serialVersionUID = 1L;
    public NewStyle() {
        setTitle("Swing class demo");
        addWindowListener(new WindowAdapter() { // anonymous inner class extending WindowAdapter - cannot be replaced by lambda due to multiple methods
            public void windowClosing(WindowEvent e) {
                JOptionPane.showMessageDialog(NewStyle.this, "We are exiting"); // reference to outer class instance
            }
        });
        getContentPane().setLayout(new BorderLayout());
        JButton btn = new JButton("Click here");
        btn.addActionListener((ActionEvent e) -> { // lambda replacing anonymous inner class
            JOptionPane.showMessageDialog(NewStyle.this, "You clicked"); // reference to outer class instance
        });
        getContentPane().add(btn, BorderLayout.CENTER);
        pack();
    }
    public static void main(String[] args) {
       SwingUtilities.invokeLater(() -> { // lambda replacing anonymous inner class
            JFrame f = new NewStyle();
            f.setVisible(true);
        });
    }
}

Note that the lambda is compiled into an anonymous inner class in the byte code.

Method local inner classes:

It is also possible to define an inner class inside a method. And again as inner class it has access to outer.

Example:

package classes.methodinner;

public class Demo {
    private int v;
    public int getV() {
        return v;
    }
    public void setV(int v) {
        this.v = v;
    }
    public void test() {
        class Other {
            private int x;
            public int getX() {
                return x;
            }
            public void setX(int x) {
                this.x = x;
            }
            public void test() {
                System.out.printf("Outer v = %d\n",  getV());
            }
        }
        Other other = new Other();
        other.setX(456);
        System.out.printf("Other x = %d\n", other.getX());
        other.test();
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.setV(123);
        System.out.printf("Demo v = %d\n", demo.getV());
        demo.test();
    }
}

I have never used one of these. I don't see the point.

enum:

Java 5 added enum.

In most languages enum is really just an int and can more or less easily be used as an int. But not in Java - in Java enum is a special class with a fixed number of instances - and object instances can notbe used as an int.

Regular example:

package classes.enum5;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Demo {
    public static enum Suit { Spade, Heart, Club, Diamond }
    public static enum Value {Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace }
    public static class Card {
        private Suit suit;
        private Value value;
        public Card(Suit suit, Value value) {
            this.suit = suit;
            this.value = value;
        }
        public Suit getSuit() {
            return suit;
        }
        public Value getValue() {
            return value;
        }
        @Override
        public String toString() {
            return String.format("(%s,%s)",  suit, value);
        }
    }
    private static Random rng = new Random();
    public static void main(String[] args) {
        List<Card> deck = new ArrayList<Card>();
        for(Suit s : Suit.values()) {
            for(Value v : Value.values()) {
                deck.add(new Card(s, v));
            }
        }
        System.out.println(deck.get(rng.nextInt(deck.size())));
    }
}

Totally understandable code.

But as explained above, then Java enum is really a class and it can be used/abused as such.

Example:

package classes.enum5.bad;

public enum Demo {
    A("This is A", 1), B("This is B", 2), C("This is C", 3);
    private String sval;
    private int ival;
    private Demo(String sval, int ival) {
        this.sval = sval;
        this.ival = ival;
    }
    public void test() {
        System.out.printf("%s - %d\n", sval, ival);
    }
    public static void main(String[] args) {
        Demo.A.test();
        Demo.B.test();
        Demo.C.test();
    }
}

I don't like to see enum used like that.

Smart people have noticed that an enum can be just a single instance and combined with the fact that it can do anythin, then enum can be used to implement singleton pattern. See code example here.

I don't like that either.

record:

Recent Java added record. It was added as preview with Java 14, became final with Java 16 and was in a LTS version with Java 17.

A Java record is a special Java class with:

Example:

package classes.record14;

public class Demo {
    public static record Data(int iv, String sv) { };
    public static void main(String[] args) {
        Data o = new Data(123, "ABC");
        System.out.printf("(%d,%s)\n", o.iv(), o.sv());
        System.out.println(o);
    }
}

Easy syntax for data classes.

It is possible to add extra methods like for any other class.

Example:

package classes.record14.ok;

public class Demo {
    public static record Data(int iv, String sv) {
        public void test() {
            System.out.printf("test iv=%d sv=%s", iv, sv);
        }
    }
    public static void main(String[] args) {
        Data o = new Data(123, "ABC");
        o.test();
    }
}

Article history:

Version Date Description
1.0 September 5th 2024 Initial version
1.1 September 7th 2024 Add enum and record sections

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj