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.

  1. Java programs are made up (entirely) of classes; code cannot exist outside of a class. (Yes, yes, I know about JShell, that’s different.)
  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.
  3. An object is an instance of a class.
    class Circle { }             // A class
    
    var c1 = new Circle();       // an instance
    var c2 = new Circle();       // another instance
    
  4. A class has members. Members can be fields, methods, or constructors. Classes can also contain instance initializers and static initializers (though these are rare).
    class Dog {
        private String name;                      // field
        public Dog(String n) { name = n; }        // constructor
        public String getName() { return name; }  // method
    
        static { System.out.println("Dogs!"); }   // static initializer (rare)
        { System.out.println("Creating a dog"); } // instance initializer (rare)
    }
    
    Static initializers run only once, when the class is loaded. But every time an instance is created, instance initializers will be run, then the constructor.
  5. 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 getName() { return name; }                    // instance method
        public static int getDogsCreated() { return dogsCreated; }  // class method
    
        public Dog(String n) { name = n; dogsCreated++; }           // constructor
    }
    
    var pet = new Dog("Sirius");
    var fren = new Dog("Sebastian");
    System.out.println(pet.getName());            // Each dog has its own name
    System.out.println(fren.getName());           // 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.getDogsCreated());     // 2
    
  6. Constructors are not actually methods. Methods operate on existing objects. Constructors bring a new object into existence (one that wasn’t in existence before). Constructors are always invoked with the operator new.
  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. A class can extend another class (called its superclass). The subclass specializes its superclass perhaps by adding 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 getManufacturer() { return manufacturer; }
        public String getSerialNumber() { 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 getDescriptor() { return descriptor; }
        public boolean isWinter() { return winter; }
        // The methods getManufacturer and serialNumber are inherited here!
    }
    
    var t = new Tire("Michelin", "3X8946P-988Q", "225/45R17", true);
    System.out.println(t.getSerialNumber());
    System.out.println(t.isWinter());
    
  10. 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. Tires inherit the capabilities of parts. An interesting aspect of the IS-A relationship is that if we have a variable that typed as being a Part, we can assign a Tire to it!
    Part p = new Tire("Michelin", "3X8946P-988Q", "225/45R17", true);
    System.out.println(t.getSerialNumber());
    // But we cannot say p.isWinter() here! Do you see why?
    
  11. 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"};
    
  12. Because every class in Java extends Object, that means every object comes with some nice built-in behavior. Read the documentation for the class Object. There are four methods you are likely to use:
    • equals(Object)
    • toString()
    • getClass()
    • hashCode()
    and a few others you are very unlikely to use.
  13. 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 can override it, by providing a specialized implementation. This is particularly useful for toString() (and if needed, essential for equals and hashCode, but these are beyond the scope of this page.):
    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?
  14. 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 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 getColor() { 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.
  15. 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 simply a collection of methods that all of the classes must implement:
    interface Group {
        void add();
        void remove();
    }
    
    Here a group is anything that can be added to and removed from. Please note: all methods in an interface are automatically public and abstract, whether you use those modifiers or not.
  16. Good to know: 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 { ... }
    class SuperDuperCircle extends Circle implements Drawable, Saveable { ... }
    
  17. A final class cannot be subclassed. (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).
  18. Okay let’s get technical. 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();
        }
    }
    
  19. 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 modifieraccessible only within its own package
    publicaccessible wherever its package is
    abstractcannot be instantiated (may have abstract methods)
    finalcannot be extended (subclassed)
    staticmakes an inner-declared class really a top-level one
    Member Modifiers
    privateaccessible only within its class
    no modifieraccessible 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
  20. There is also 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.
  21. 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. Okay, well, that’s cool, but you rarely need this stuff.
  22. 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;
        ...
    }
    
  23. Parameterized types are instantiated in several ways:
    Greeter<Integer> r1 = new Greeter<Integer>();
    Greeter<?> r2 = new Greeter<String>();
    Greeter<? extends JComponent> r3 = new Greeter<JCheckBox>();
    Greeter<? super Graphics2D> r4 = new Greeter<Graphics>();
    
  24. Methods can be parameterized (generic), too.
    public class CollectionUtil {
        ...
        public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
            for (T o : a) {
                c.add(o);
            }
        }
    }
    
  25. For more on parameterized types see the section on generics in the Java Tutorial.
  26. 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 some 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.
  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.
    ...
    if (diceSet == null) {
        throw new IllegalStateException("You don't have any dice yet");
    }
    ...
    
  5. 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 = "🎲";
    
  6. Tell don’t ask, but you know, you don’t necessarily have to follow rules absolutely. If you must ask, use a getter, it’s okay. Well, not everyone agrees.
  7. If you find yourself using both getters and setters, you are probably 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.
  8. Always try to declare fields with an interface name, but initialize them with the implementing class:
    List<Integer> = new ArrayList<>();
    
  9. Consider 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).

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