Java foundation - internal classes

Posted by greepit on Wed, 23 Feb 2022 07:18:35 +0100

You can put the definition of one class inside the definition of another class, which is the inner class.

Benefits of using inner classes:

  • It allows you to organize some logically related classes together and control the external access rights of internal classes
  • The inner class is a code hiding mechanism. At the same time, the inner class also knows the outer class and can communicate with the outer class

1. Create internal class

The way to create an internal class is to put the definition of the internal class in the definition of the external class:

public class Parcel {
    class Contents {
        private int i = 11;
        public int value() {
            return i;
        }
    }
    class Destination {
        private String label;
        Destination(String whereTo){
            label = whereTo;
        }
        String readLabel() {
            return label;
        }
    }
    public Destination to(String s) {
        return new Destination(s);
    }
    public Contents contents() {
        return new Contents();
    }
    public void ship(String dest) {
        Contents c = contents();
        Destination d = to(dest);
        System.out.println(d.readLabel());
    }

    public static void main(String[] args) {
        Parcel p = new Parcel();
        p.ship("Tasmania");
        Parcel q = new Parcel();

        // Reference format of internal class object
        Parcel.Contents c = q.contents();
        Parcel.Destination d = q.to("XTU");
        // Since it is in the main method of the external class, the following two sentences are equivalent to the above
        // Contents c = q.contents();
        // Destination d = q.to("XTU");
    }
}

/* output
Tasmania
*///

There is a typical case in the above class. The external class has a method that will return a reference to the internal class (to method and contents method). Then why do you need such a method?

In general, the access rights of internal classes are package access rights or private class method rights. At this time, we cannot get the objects of internal classes through the construction methods of internal classes outside the package. In order to get the object of the inner class, we need to provide an interface in the outer class, which returns a reference to the inner class.

Note: in the external class, we can indicate the type of internal class object through innerclassname. However, in other classes, if you want to specify the type of an internal class object, you must declare it as outerclassname InnerClassName.

2. Link to external class

When we generate an object of an inner class, the object will establish a connection with the peripheral object that makes it. With this connection, it can easily access all members of its peripheral object. In addition, the inner class has access to all elements of its outer class.

public class Sequence {
    private Object[] items;
    private int next = 0;

    public Sequence(int size) {
        items = new Object[size];
    }
    public void add(Object x) {
        if(next < items.length) {
            items[next++] = x;
        }
    }
    private class SequenceSelector implements Selector {
        private int i;
        @Override
        public boolean end() {
            return i == items.length;
        }

        @Override
        public Object current() {
            return items[i];
        }

        @Override
        public void next() {
            if (i < items.length) {
                i++;
            }
        }
    }
    
    public Selector selector() {
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for (int i = 0; i < 10; i++) {
            sequence.add(Integer.toString(i));
        }
        Selector selector = sequence.selector();
        while (!selector.end()) {
            System.out.print(selector.current() + " ");
            selector.next();
        }
    }
}
/* output
0 1 2 3 4 5 6 7 8 9 
*///

The above example well reflects the characteristic that the internal class has access to all elements of its peripheral class. The internal class SequenceSelector is private. In order to get its object, we can only call the external class selector interface. The obtained object will be bound with the external class object that makes it. At this time, it can traverse the items array of the external class object. This is an example of * * iterator design pattern * *.

So how does an inner class automatically have access to all members of its outer class?

When an object of an external class creates an internal class object, the internal class object must secretly capture a reference to that external class object. Then, when you access the external class element, it is the reference to select the element of the external class. But fortunately, the compiler will help us get rid of all the details. Therefore, we can see that the object of the internal class can be created only when it is associated with its external class object (when the internal class is non static). There needs to be a reference to its external class object in the time-consuming internal class object. If the compiler cannot access this reference, it will report an error, but these do not need the programmer to worry about.

3. .this and new

If you want to get the reference of the current external class object in the definition of the internal class, you can use the external class name Obtained in the form of this. The resulting reference will automatically have the correct type, and this point is known and checked during compilation, so there is no runtime overhead.

If you want to call the constructor to create an object of an inner class (non static class), first you need to have an outer class object, and then the outer class object Create an internal class object in the form of new internal class name (). At this time, the internal class object will be secretly connected to the external class object that created it.

4. Anonymous inner class

  • The name of an abstract class or method that implements an anonymous parameter is {new}; The object reference of this anonymous inner class of new will be automatically transformed upward into the reference of interface or abstract class.

  • A getA(Object s){
        return new A(){
          // If this s is not used in the constructor of A, but elsewhere in the class. Then the compiler thinks that the parameter s is final,
          // You can only read but not write. You can't point s to other objects. Readers can try to speculate with the knowledge of Object references and parameters passed between methods
          // Why is it designed like this
          // s = new String("123");     Compiler error
        };
    }
    
  • Constructors cannot exist in anonymous inner classes. The reason is very simple. There is no constructor whose name comes from

Wonderful linkage between anonymous inner class and factory mode

interface Service {
    void method1();
    void method2();
}
interface ServiceFactory {
    Service getService();
}

class Implementation1 implements Service{
    private Implementation1(){}
    @Override
    public void method1() {
        System.out.println("Implementation1 method1");
    }

    @Override
    public void method2() {
        System.out.println("Implementation1 method2");
    }
    
    public static ServiceFactory factory = new ServiceFactory() {
        @Override
        public Service getService() {
            return new Implementation1();
        }
    };
}
class Implementation2 implements Service{
    private Implementation2(){}
    @Override
    public void method1() {
        System.out.println("Implementation2 method1");
    }

    @Override
    public void method2() {
        System.out.println("Implementation2 method2");
    }
    public static ServiceFactory factory = new ServiceFactory() {
        @Override
        public Service getService() {
            return new Implementation2();
        }
    };
}
public class Factories {
    public static void serviceConsumer(ServiceFactory factory){
        Service service = factory.getService();
        service.method1();
        service.method2();
    }
    public static void main(String[] args) {
        Factories.serviceConsumer(Implementation1.factory);
        Factories.serviceConsumer(Implementation2.factory);
    }
}
/* output
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
*///

In the above example, the construction methods of Implementation1 and Implementation2 classes are private. At this time, in order to get their objects, you can or can only use the getService method of factory. At the same time, for a specific service, in fact, there should be only one factory to create the same service separately. Therefore, it is created as a static domain in a specific service.

Classes are preferred over interfaces. If you need an interface in your design, you must understand it. Otherwise, you can't put it into your design unless you have to.

5. Nested classes

If you do not need a connection between the inner class object and its outer class object, you can declare the inner class as static. The inner class in this case is often called a nested class.

A normal inner class object implicitly holds a reference to the outer class object that created it. However, when the inner class is static, the situation is different.

  • You do not need to rely on objects of external classes to create objects of nested classes
  • Non static peripheral class elements cannot be accessed from objects of nested classes

Note: ordinary inner classes cannot have static data and static fields, nor can they contain nested classes. But nested classes can contain everything.

Classes inside the interface

Nested classes can be part of an interface because any class placed in an interface is automatically public and static. At the same time, the classes placed in the interface can be shared by all different implementations of the interface, which means that the code you want to be included by all implementations can be written in the class.

6. Why internal classes are needed

Each inner class can independently inherit from an implementation, so whether the outer class has inherited an implementation or not has no impact on the inner class.

The problem of "multiple inheritance" from multiple classes can be solved indirectly by using internal classes.

class D{}
abstract class E{}
class Z extends D{
    E makeE(){
        return new E(){};
    }
}
public class MultiImplementation{
    static void takesD(D d){}
    static void takesE(E e){}
    public static void main(String[] args){
        Z z = new Z();
        takesD(z);
        takesE(z.makeE());
    }
}

If you use inner classes, you can also get some other features:

  • The inner class can have multiple instances. Each instance has its own state information and is independent of the information of the outer class object
  • In an external class, multiple internal classes can implement the same interface or inherit from the same class in different ways
  • The inner class has no confusing "is-a" relationship. It is an independent individual.

6.1 closure and callback

A closure is a callable object that records information from the scope in which it was created. Through this definition, we can see that the internal class is an object-oriented closure.

Through the callback, the object can carry some information, which runs it to call the initial object at a later time. In Java, it is simply to set a member variable in a class as a callback object, and then the object of this class calls a method of the callback object at a specific time.

interface Incrementable{
    void increment();
}
class Callee1 implements Incrementable{
    private int i = 0;
    @Override
    public void increment() {
        System.out.println(++i);
    }
}
class MyIncrement {
    public void increment() {
        System.out.println("Other operation");
    }
    public static void f(MyIncrement mi){
        mi.increment();
    }
}
class Callee2 extends MyIncrement{
    private int i = 0;

    @Override
    public void increment() {
        super.increment();
        System.out.println(++i);
    }
    private class Closure implements Incrementable{
        @Override
        public void increment() {
            Callee2.this.increment();
        }
    }
    Incrementable getCallbackRefrence() {
        return new Closure();
    }
}
class Caller {
    private Incrementable callbackRef;

    public Caller(Incrementable callbackRef) {
        this.callbackRef = callbackRef;
    }
    void go() {
        callbackRef.increment();
    }
}
public class Callbacks {
    public static void main(String[] args) {
        Callee1 c1 = new Callee1();
        Callee2 c2 = new Callee2();
        MyIncrement.f(c2);
        Caller caller1 = new Caller(c1);
        Caller caller2 = new Caller(c2.getCallbackRefrence());
        caller1.go();
        caller1.go();
        caller2.go();
        caller2.go();
    }
}
/* output
Other operation
1
1
2
Other operation
2
Other operation
3
*///

In the above example, you can further see the difference between the external class implementing an interface and the internal class implementing an interface, and you can also see the use of callback. The reference named callbackRef is a callback object of the Caller class. When you call the go method of the Caller object, it will call back the increment method referenced by callbackRef.

6.2 internal category and control framework

An application framework is a class or group of classes designed to solve a specific problem. To apply an application framework, you usually inherit one or more classes and override some of their methods. In the covered method, write code to customize the general solution provided by the application framework.

Design pattern always separates the changed things from the unchanged things. In the template method, the template method is the unchanged things, and the covered method is the changed things.

Control framework is a kind of special application framework, which is used to solve the needs of responding to events (typically Swing). Its job is to execute the event when the event is "ready", but the control framework does not provide specific execution content, which is provided through inheritance.

Event class:

public abstract class Event {
    private long eventTime;
    protected final long delayTime;

    public Event(long delayTime) {
        this.delayTime = delayTime;
        start();
    }
    public void start(){
        eventTime = System.nanoTime() + delayTime;
    }
    public boolean ready(){
        return System.nanoTime() >= eventTime;
    }
    public abstract void action();
}

Event is an abstract class, which can be inherited by any concrete event. At the same time, when creating a specific object of an event, the start method will be called, which means to start the timer. The ready method determines whether it is time to respond to the event. The action method is the specific logic to respond to the event, which can be executed only when the ready method returns true.

The actual control framework that manages and triggers events: the controller class

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

public class Controler {
    private List<Event> eventList = new ArrayList<>();
    public void addEvent(Event c){
        eventList.add(c);
    }
    public void run() {
        while (eventList.size() > 0) {
            // Cannot remove while traversing in container
            for(Event e : new ArrayList<>(eventList)) {
                if(e.ready()){
                    System.out.println(e);
                    e.action();
                    eventList.remove(e);
                }
            }
        }
    }
}

You can add events to the framework through the addEvent method. At the same time, the run method loops through the Event list to find the ready Event object that can be run. Call the toString() method of the object to print out the object, call its action() method, and finally remove it from the Event list.

In the current design, we don't know what Event does. This is also the key to the design, "separating the changing things from the invariable things", "change vector" is the different behavior of different Event objects, which is also what is done by creating different Event subclasses.

  • The complete implementation of the control framework is created by a single class, so that the details of the implementation are encapsulated. Internal classes are used to solve various action s () necessary for the problem.
  • The inner class can easily access any member of the outer class, so it is easy to write the corresponding code
public class GreenhouseControls extends Controler{
    private boolean light = false;
    public class LightOn extends Event {
        public LightOn(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            light = true;
        }

        @Override
        public String toString() {
            return "Light is on";
        }
    }
    public class LightOff extends Event {
        public LightOff(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            light = false;
        }

        @Override
        public String toString() {
            return "Light is off";
        }
    }
    private boolean water = false;
    public class WaterOn extends Event {
        public WaterOn(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            water = true;
        }

        @Override
        public String toString() {
            return "GreenHouse Water is on";
        }
    }
    public class WaterOff extends Event {
        public WaterOff(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            water = false;
        }

        @Override
        public String toString() {
            return "GreenHouse Water is off";
        }
    }
    public class Bell extends Event{
        public Bell(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            addEvent(new Bell(delayTime));
        }

        @Override
        public String toString() {
            return "Bing!";
        }
    }
    public class Restart extends Event{
        private Event[] eventList;
        public Restart(long delaytime, Event[] eventList) {
            super(delaytime);
            this.eventList = eventList;
            for(Event e : eventList) {
                addEvent(e);
            }
        }

        @Override
        public void action() {
            for(Event e : eventList) {
                e.start();
                addEvent(e);
            }
            start();
            addEvent(this);
        }

        @Override
        public String toString() {
            return "Restarting system";
        }
    }
    public static class Terminate extends Event {
        public Terminate(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            System.exit(0);
        }

        @Override
        public String toString() {
            return "Terminating";
        }
    }
}

// An example of command design pattern
public class GreenhouseController {
    public static void main(String[] args) {
        GreenhouseControls gc = new GreenhouseControls();
        gc.addEvent(gc.new Bell(900));
        Event[] eventList = {
                gc.new LightOn(200),
                gc.new LightOff(200),
                gc.new WaterOn(200),
                gc.new WaterOff(200)
        };
        gc.addEvent(gc.new Restart(2000, eventList));
        // be unable to stop
        gc.run();
    }
}

From the above example, we can see the value of internal classes, especially when using internal classes in the control framework, which is very elegant.

Reasons for using local inner classes instead of anonymous inner classes:

  • We need a named constructor or a constructor that needs to be overloaded
  • We need more than one object of this inner class

If you have time, it is strongly recommended that readers with a certain Java foundation chew the book of Java programming ideas. Although there are many words and the book is small, this book is definitely the best choice to enrich the basic knowledge and advanced level of Java.

Topics: Java Back-end