Classes in Java

Here is most of what you need to know about Java classes, presented in micro lessons.

Unit Goals

To be able to use Java classes effectively in your own programs, and be able to talk about Java classes in rather technical terms.

A Technical Tour

Technical details below! Some of the details are super important. Some you should memorize. Some are rather hard to memorize, but that’s okay; you can just look them up when needed. The point of this section is to act as an almost complete reference, rather than a tutorial.

  1. An object is an instance of a class.
    class Circle { }             // A class
    
    var c1 = new Circle();       // an instance
    var c2 = new Circle();       // another instance
    
  2. A class has members. Members can be fields, methods, or constructors. Classes can also contain instance initializers and static initializers (though these are rare). Static initializers run only once, when the class is loaded. Instance initializers, then constructors, are run every time an instance is created.
    class Dog {
        String name;                              // field
        Dog(String n) {                           // constructor
             name = n;
             System.out.println(n);
        }
        String bark() {                           // method
            return name + " says woof!";
        }
        static { System.out.println("Dogs!"); }   // static initializer (rare)
        { System.out.print("Creating dog "); }    // instance initializer (rare)
    }
    
                                   // prints "Dogs!"
    var d1 = new Dog("Nimbus");    // prints "Creating dog Nimbus"
    var d2 = new Dog("Lucy");      // prints "Creating dog Lucy"
    d2.bark()                      // "Lucy says woof!"
    
  3. You can never use an object directly in Java; only references to objects are stored.
  4. 2dogs.png

  5. It is common in Java to mark constructors, fields, and methods with an access modifier like private or public. If private, members are only available to code within the class; if public, they can be used outside the class. (Other modifiers will be described later.)
    class Dog {
        // Make the field private so no one can change it from outside the class
        private String name;
    
        // Constructors and methods public so anyone can create dogs and make them bark
        public Dog(String n) { name = n; }
        public String bark() { return name + " says woof!"; }
    }
    
  6. Fields can be instance fields or class fields. Methods can be instance methods or class methods. Every instance of the class has its own copies of the instance fields, but all instances share the class fields. In a sense, class fields belong to the class, not to each instance. Ditto for methods.
    class Dog {
        private String name;                                        // instance field
        private static int dogsCreated;                             // class field
    
        public static final String species = "Canis familiaris";    // another class field
    
        public String name() { return name; }                       // instance method
        public static int dogsCreated() { return dogsCreated; }     // class method
    
        public Dog(String n) { name = n; dogsCreated++; }           // constructor
    }
    
    var pet = new Dog("Sirius");
    var friend = new Dog("Sebastian");
    System.out.println(pet.name());            // Each dog has its own name
    System.out.println(friend.name());         // Each dog has its own name
    System.out.println(Dog.species);           // all dogs share a species
    System.out.println(pet.species);           // legal, but uncommon
    System.out.println(Dog.dogsCreated());     // 2
    
  7. Within constructors and instance methods, you can refer to the instance that is being operated on with the this keyword. This is particularly useful in constructors, where parameters often have the same name as fields!
  8. class Location {
        private double latitude;
        private double longitude;
        public Location(double latitude, double longitude) {
            this.latitude = latitude;
            this.longitude = longitude;
        }
    }
    
  9. An important consequence of the fact that variables can only store references to objects is that the == operator on objects only checks if the references refer to the same object. It never compares the actual contents:
  10. 3vars2objs.png

    var p1 = new Location(37.6308, 119.0326);
    var p2 = p1;
    var p3 = new Location(37.6308, 119.0326);
    
    p1 == p2; // true
    p1 == p3; // false
    
  11. A class can extend another class. The new class is called a subclass and the class it extends is called a superclass. The subclass inherits the non-private fields and methods of the superclass, and it may optionally add new fields and methods. The constructor for the subclass often uses the constructor of the superclass. Here’s an example:
    class Part {
        private String manufacturer;
        private String serialNumber;
        public Part(String manufacturer, String serialNumber) {
            this.manufacturer = manufacturer;
            this.serialNumber = serialNumber;
        }
        public String manufacturer() { return manufacturer; }
        public String serialNumber() { return serialNumber; }
    }
    
    class Tire extends Part {
        private String descriptor;
        private boolean winter;
        public Tire(String manufacturer, String serialNumber, String descriptor, boolean winter) {
            super(manufacturer, serialNumber);
            this.descriptor = descriptor;
            this.winter = winter;
        }
        public String descriptor() { return descriptor; }
        public boolean isWinter() { return winter; }
        // The methods manufacturer and serialNumber are inherited here!
    }
    
    var t = new Tire("Michelin", "3X8946P-988Q", "225/45R17", true);
    System.out.println(t.serialNumber());
    System.out.println(t.isWinter());
    
  12. We use class extension to model the IS-A relationship between classes. Here a tire IS-A part. So everything we can do with parts, we can do with tires. What’s cool about this is that if you type-constrain a variable to a Part, you can assign a Tire to it, because a tire is a part!
    Part p = new Tire("Michelin", "3X8946P-988Q", "225/45R17", true);
    System.out.println(p.serialNumber());
    
  13. When type-constraining a variable to the superclass, you are narrowing the view of the object. The object under the hood has the subclass as its true class, but you can only “see” the parts accessible through the superclass lens:
    Part p = new Tire("Michelin", "3X8946P-988Q", "225/45R17", true);
    System.out.println(p.serialNumber()); // OK
    // p.isWinter() // ILLEGAL!!! Do you see why?
    
  14. Every class in Java implicitly is a subclass of the predefined class called Object (well...every class except Object itself.) You don’t have to say extends Object—that happens for free. So we can write this:
    class Dog {}
    Object d = new Dog();
    Object s = "Strings are objects too";
    Object[] things = new Object[]{new Dog(), "Hello"};
    
  15. The class Object includes a number of its own methods. that all classes inherit. These include equals(), hashCode(), toString(), getClass(), wait(), notify(), notifyAll(), and clone(). Not all of these are commonly used.
  16. When a method is defined in a superclass, you can inherit it directly (like we did above in the Part and Tire example), or you may be able to override it, by providing a specialized implementation:
    class Point {
        private double x;
        private double y;
        public Point(double x, double y) { this.x = x; this.y = y; }
        @Override public String toString() { return "(" + x + "," + y + ")"; }
    }
    
    var p = new Point(5, 8);
    System.out.println(p);     // Automatically invokes p.toString() ...cool right?
    
    Exercise: How would points print without overriding toString?
  17. You can mark a method final in a superclass. A final method cannot be overriden in a a subclass. Interestingly, but very sensibly, getClass, wait, notify, and notifyAll are all marked final in Object. (This makes sense, as overriding getClass for example, would be plain nuts.) However you absolutely do want subclasses to provide their own overridden versions of equals, hashCode, and toString.
  18. You frequently override toString to make a nice printable string representation of an object, because the default toString is pretty ugly. The default for equals is just == so if you want to compare fields, overriding equals is necessary. (For example the built-in String class overrides this to compare characters.) There is a rule in Java that says If you override equals you must also override hashCode. If you don’t, putting your objects in sets and maps won’t work right. The pattern for overriding equals and hashCode is not so bad, just follow this example:
    class Point {
        private double x;
        private double y;
        public Point(double x, double y) { this.x = x; this.y = y; }
        public double x() { return x; }
        public double y() { return y; }
        @Override public boolean equals(Object o) {
            return (o instanceof Point other) && x == other.x && y == other.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
        @Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
    }
    
  19. In practice, this business of value-equality and storing items in sets and as map keys only really makes sense for immutable objects. Therefore, Java provides a special kind of class, called the record in which all fields are private, you get accessors for each, and equals, hashCode, and toString are automatically overridden for you to do the right thing! So that big ugly class in the previous item could be written instead as:
    record Point(double x, double y) {}
    
  20. How important is overriding equals and hashCode?

    You probably won’t need to much in practice. If you make mutable data structures, they tend to be one-off objects for storing and retrieving information to help you with some larger application. You almost never build data stores like this for the purposes of seeing if they are equal to other stores.

    But small data objects, like points and vectors and big numbers and lines and colors, for which you make lots of instances, will likely be put in sets or compared by value. But in this case, just use records and you will be fine.

    That said, you should be aware of how wonky Java is in this area, so you understand the code you write.

    Are records totally immutable?

    They are only shallowly immutable. Shallowness is something you see when you talk about deep vs. shallow copy, deep vs. shallow equality, and deep vs. shallow immutability. Details in class. Records only give you shallow benefits, but that’s still great.

  21. Okay, back to the class extension / inheritance thing. Sometimes you do not want to extend a class at all! A final class cannot be subclassed. For example, the class String is final. Can you see why? Subclassing String would only be confusing to people; strings should all work the same for everyone. To declare one of your own classes final:
    final class Atom { }
    
    Extending Atom is now an error.
  22. Exercise: Explain the difference between a final method and a final class in your own words.
  23. Sometimes the superclass only exists to generalize a bunch of other classes, rather than having instances of its own. In this case, the superclass should be marked abstract. An abstract class is not allowed to have any instances; if you try instantiating with new, you’ll get an error. Instead, you instantiate only its subclasses. Here’s a good example. Circles and rectangles are both shapes and we know how to compute their areas and perimeters. But there’s no such thing as a “plain” shape, only specific kinds of shapes. Study the example:
    abstract class Shape {
        private String color;
        public Shape(String color) { this.color = color; }
        public String color() { return color; }
        public abstract double area();
        public abstract double perimeter();
    }
    
    class Circle extends Shape {
        private double radius;
        public Circle(String color, double radius) {
            super(color);
            this.radius = radius;
        }
        @Override public double area() { return Math.PI * radius * radius; }
        @Override public double perimeter() { return 2 * Math.PI * radius; }
    }
    
    class Rectangle extends Shape {
        private double length;
        private double width;
        public Rectangle(String color, double length, double width) {
            super(color);
            this.length = length;
            this.width = width;
        }
        @Override public double area() { return length * width; }
        @Override public double perimeter() { return 2 * (length + width); }
    }
    
    Exercise: Explain in your own words why shape needs to be an abstract class.
    Exercise: Investigate what would happen if you left out the definition of area in the Rectangle class.
    Exercise: Why does it make no sense for a class to be both final and abstract?
  24. Sometimes, classes are related by having the same behaviors, but there is no superclass at all! In this case we tie the related classes together with an interface, which is basically a collection of methods that classes must implement:
    interface Group {
        void add();
        void remove();
    }
    
    Here a group is anything that can be added to and removed from.
    Interface member modifiers

    All methods in an interface are automatically public, whether you mark them public or not!

    Unless marked static or default, an interface method will be automatically abstract, whether you mark it abstract or not.

  25. A class is only allowed to extend at most one superclass, but it can implement zero or more interfaces:
    class Shape { ... }
    class Circle extends Shape { ... }
    interface Drawable { ... }
    interface Saveable { ... }
    interface Movable { ... }
    class SuperDuperCircle extends Circle implements Drawable, Saveable, Movable { ... }
    
  26. Interfaces differ from abstract classes in that interfaces can not specify instance fields! So if you wanted to specify shared fields among classes, you would need abstract classes. If you are only specifying common behaviors (and not common state), use interfaces:
    interface Solid {
        double volume();
    }
    record Sphere(double radius) implements Solid {
        double volume() { return 4.0 / 3.0 * Math.PI * Math.pow(radius, 3); }
    }
    record Box(double width, double height, double depth) implements Solid {
        double volume() { return width * height * depth; }
    }
    
  27. In addition to methods that implementing classes must override (the interface’s abstract methods), interfaces can also have default methods:
    interface Collection {
        void add(Object o);
        void remove(Object o);
        int size();
        default boolean isEmpty() { return size == 0; }
    }
    
  28. Interfaces can also have static methods which are basically just global functions:
    interface Geometry {
      static double circleArea(double r) { return Math.PI * r * r; }
      static double squareArea(double side) { return side * side; }
      static double perimeter(double side) { return side * 4.0; }
      static double boxVolume(double w, double h, double d) { return w * h * d; }
    }
    

    Now you can simple call Geometry.perimeter(2) for example.

    More about Interfaces

    Check out the section on interfaces from the Java Tutorial.

  29. Classes and interfaces can be sealed, to allow only a fixed number of specific subclasses or implementing classes:
    public sealed interface BinaryTree permits EmptyTree, BinaryTreeNode {
        int size();
    
        // More methods...
    }
    
    final class EmptyTree implements BinaryTree {
        public int size() {
            return 0;
        }
        // ...
    }
    
    final class BinaryTreeNode implements BinaryTree {
        private String data;
        private BinaryTree left;
        private BinaryTree right;
    
        public int size() {
            return left.size() + right.size() + 1;
        }
        // ...
    }
    
  30. An enum is a class with a fixed number of instances:

    enum ArticleStatus {
        DRAFT, PUBLISHED, REDACTED, REPLACED;
    }
    

    Now ArticleStatus.DRAFT is one of the four instances of ArticleStatus. There is much more to enums to know, but this above is a start.

  31. A fun table:
    Zero AllowedFixed Number of
    InstancesAbstract ClassEnum
    SubclassesFinal ClassSealed Class
  32. There is a class called Class. Every class is an object of the class Class. You can get the class of an object x with x.getClass(). If c is the name of a class (or a primitive type) you can write c.class.
    "hello".getClass() == String.class  // true
    

Advanced Stuff

Feel free to skip the following micro-items on a first read.

  1. There are five kinds of classes: package-level, nested top-level, member, local, or anonymous. (The last four kinds are called inner classes.

    ClassExample.java
    /**
     * A throw-away class that illustrates the five kinds of Java classes.
     * 'A' is a package-level class, 'B' is a nested top-level class,
     * 'C' is a local class, 'D' is a local class, and there is an
     * anonymous class that is a subclass of Thread.
     */
    class A {
        int x;
    
        static class B {
            int x;
        }
    
        class C {
            int x;
            int sum() {return x + A.this.x;}
        }
    
        void f() {
            class D {
               int x;
            }
            D d = new D();
            Thread e = new Thread() {
                public void run() {
                    System.out.print("Hello from " + getName());
                }
            };
            System.out.println(d.x);
            e.start();
        }
    }
    
    /**
     * Application that shows off how one would use the demo class above.
     */
    public class ClassExample {
        public static void main(String[] args) {
            A a = new A();
            a.x = 1;
            A.B b = new A.B();
            b.x = 2;
            A.C c = a.new C();
            c.x = 3;
            System.out.println(c.sum());
            a.f();
        }
    }
    
  2. Classes live in packages. There are 5,000 or so classes built-in to Java, and programmers have written hundreds of thousands if not millions of their own. If we didn’t have packages to group classes, we’d have trouble organizing things. Now, you may have seen classes written without a package: these are assigned to what is called the (unnamed) “default package”. This package is great for learning and playing around, but real Java code never uses it.

    See the official documentation for:

  3. Here’s some good reference information. In the declaration of a class or member the entity can be marked with zero or more modifiers. (But private, protected and public are supposed to be mutually exclusive.)

    Class Modifiers
    (no modifier)accessible only within its own package
    publicaccessible wherever its package is
    abstractcannot be instantiated (may have abstract methods)
    finalcannot be extended (subclassed)
    sealedhas only the subclasses indicated
    staticmakes an inner-declared class really a top-level one
    Member Modifiers
    privateaccessible only within its class
    (no modifier)accessible only within its package
    protectedaccessible only within its package and to its subclasses
    publicaccessible wherever its class is
    Field Modifiers
    finalvalue may not be changed, once assigned
    staticonly one instance of the field shared by all objects of the class
    transientfield is not serialized
    volatilevalue may change asynchronously compiler must avoid certain optimizations
    Method Modifiers
    finalmay not be overridden
    staticmethod does not apply to a particular instance
    abstracthas no body; subclasses will implement
    synchronizedrequires locking the object before execution
    nativeimplementation is not written in Java, but rather in some platform dependent way
  4. Every object in Java has a monitor which can be locked and unlocked by entering and leaving, respectively, a synchronized statement. In addition, condition synchronization comes for free since wait() and notify() are methods of the class Object.
  5. Classes can be parameterized with types (a parameterized type is also called a generic type):
    class Greeter<V>; {
        private V target;
        // ...
    }
    
    class Pair<T1, T2> {
        private T1 first;
        private T2 second;
        // ...
    }
    
  6. Wildcards assist in making supertype-supertype relationships among parameterized types:
    Greeter<Integer> r1 = new Greeter<Integer>();
    Greeter<?> r2 = new Greeter<String>();
    Greeter<? extends Animal> r3 = new Greeter<Dog>();
    Greeter<? super Dog> r4 = new Greeter<Animal>();
    
  7. We need wildcards because Java wisely refuses to make List<Dog> a superclass of List<Animal>. If it did define subclasses that way, something called covariance, we would get horribleness:
  8. List<Dog> dogs = new ArrayList<Dog>();
    
    List<Animal> animals = dogs;  // static type list-of-animal, actual class list-of-dog
    animals.add(new Cat("Fluffy"));  // compiler *would* allow this in a covariant world!
    
  9. Java does not define any kind of subclass relationship between the types P<T> and P<U> even when T and U are related. Therefore we say Java generic types are invariant.
  10. Methods can be parameterized (generic), too. Put the generic type parameters before the method return type.
    public class CollectionUtils {
        // ...
        public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
            for (T o : a) {
                c.add(o);
            }
        }
    }
    
  11. For more on parameterized types see the section on generics in the Java Tutorial. There is also a nice overview of the basics at Baeldung. You might also find the quite old In-depth generics course by Angelika Langer useful too.
  12. A functional interface is an interface with a single abstract method. You can write objects of such an interface directly with a lambda expression. There are many functional interfaces in the standard library.
    InterfaceMethodDescription
    Function<T,R>applyFunction of one T argument producing an R result
    Consumer<T>acceptFunction of one T argument producing no result
    Supplier<T>getFunction of no arguments producing a T result
    Predicate<T>testFunction of one T argument producing a boolean result
    BiFunction<T,U,R>applyFunction of two arguments producing an R result
    BiConsumer<T,U>applyFunction of two arguments producing no result
    BiPredicate<T>testFunction of two arguments producing a boolean result
    UnaryOperator<T>applysame as Function<T,T>
    BinaryOperator<T>applysame as BiFunction<T,T,T>
    Runnablerunfunction with no arguments and no results
    Comparator<T>comparefunction of two arguments returning an int.
  13. Because Java has the no-primitives restriction for generics, there are quite a few additional functional interfaces with primitives for arguments and/or results. Examples:
    • IntFunction<R>, IntConsumer, IntSupplier, IntPredicate, IntUnaryOperator, IntBinaryOperator
    • DoubleFunction<R>, DoubleConsumer, DoubleSupplier, DoublePredicate, DoubleUnaryOperator, DoubleBinaryOperator
    • LongFunction<R>, LongConsumer, LongSupplier, LongPredicate, LongUnaryOperator, LongBinaryOperator
    • IntToDoubleFunction, IntToLongFunction, DoubleToIntFunction, DoubleToLongFunction, LongToIntFunction, LongToDoubleFunction
  14. To write non-trivial Java applications, you need to understand a little bit about classloaders and classpaths. To use Java in your study of basic programming, data structures, and algorithms, and perhaps to do some competitive programming and interview practice, you probably don’t. But you should look into such things; they are very cool.

Best Practices

What should you think about when building your Java application with classes? Here are ten best practices:

  1. Sketch out the classes you are going to create on paper or whiteboard or tablet.

    polygonuml.png

  2. When writing a constructor or method, remember it will either succeed or fail. if it fails, throw an exception. (By “fail” we mean “really fail” since sometimes optionals and result objects might fit your usecase better.)
  3. One of the most common exceptions you will throw in IllegalArgumentException. Everytime you write a constructor or method that accepts parameters, ask yourself whether the argument being passed in is might be meaningless (or malicious!) and immediately just throw an IllegalArgumentException.
    public class Die {
        private final int sides;
        private int value;
    
        public Die(int sides, int value) {
            if (sides < 4) {
                throw new IllegalArgumentException("At least four sides required");
            }
            if (value < 1 || value > sides) {
                throw new IllegalArgumentException("Die value not legal for die shape");
            }
            this.sides = sides;
            this.value = value;
        }
    
        // ...
    }
    
  4. IllegalStateException is also common.
    public void add(Object item) {
        if (isFull()) {
            throw new IllegalStateException("Cannot add to full collection");
        }
    
        // ...
    }
    
  5. Exercise: In your own words, explain when you should use IllegalArgumentException and when you should use IllegalStateException.
  6. When doing validation at the beginning of a method, make use, where possible, of some of the really nice static methods in Objects. These do a check and throw if the check fails, saving you from having to write an ugly if statement:
    Objects.requireNonNull(e);
        // throws NullPointerException if e is null
    
    Objects.requireNonNull(e, message);
        // throws NullPointerException with the given message if e is null
    
    x = Objects.requireNonNullElse(e1, e2);
        // returns e1 if e1 is non-null, otherwise returns e2 (as a backup, so to speak)
    
    checkIndex(e, length);
        // throws IndexOutOfBoundsException if e < 0 or e >= length
    
  7. Almost always, you should keep fields private, and operate on them with methods. An exception to this is “constants”, which are public, static, final, and written in all caps.
    public static final String SIX_SIDED_DIE_EMOJI = "🎲";
    
  8. Tell don’t ask, but you know, you don’t necessarily have to follow rules absolutely. If you must ask, use an accessor, it’s okay. Well, not everyone agrees.
  9. If you find yourself using so-called getters and setters, which directly read and write fields, you are most likely doing something wrong. A getter and a setter for a field (that simply reads and write the field, anyway) seems like a massive waste of energy to manipulate what is essentially a simple variable.
  10. Always try to declare fields, method parameters, and method return types with an interface name. This narrowing is very useful because it allows you to trivially replace the right-hand-side with a different class, and the rest of your code stays the same:
    List<Integer> items = new ArrayList<>();
    
  11. Always favor immutability. Making a class immutable takes a little bit of effort. Follow these four rules:
    • Provide absolutely NO mutators.
    • Ensure no methods can ever be overridden: either make the class final, make every method final, or hide the constructors and expose only static factory methods.
    • Make all fields private and final.
    • Watch out for mutable components! If you have a field of a mutable class make a defensive copy in constructors, accessors and the method readObject (if you are overriding that from streams).
    Records can get you there, as long as your record fields are not themselves mutable; if a record has mutable fields you are on the hook for satisfying the previous bullet point.

Challenge Problem

Try to figure out what this program does, without running it. After you think you have your answer, run it to see if you are right.

T.java
public class T {
    private final String name;

    private void printName() {
        System.out.println(name);
    }

    public T(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        new T("main").doIt();
    }

    private void doIt() {
        new T("doIt") {
            void method() {
                printName();
            }
        }.method();
    }
}

Summary

We’ve covered:

  • Technical details of Java classes
  • Some best practices for using Java classes