主要介绍内部类,顺便提一下接口中的嵌套接口和嵌套类。

1.内部类

内部类是定义在其它类中的类。这个定义很宽泛,因为内部类也可以定义在类中的方法里,可以用匿名内部类,还可以声明为public、private、protected、static,不同的情形下,内部类表现也不一致,分别举例说明。

1.1 public 内部类

这是最普遍的情形,在一个类的内部定义另一个类,假设两个类分别叫AInnerA,此时InnerA可以访问A中的属性,其它类也可以通过A的实例来实例化InnerA,因为InnerA是声明为public。为什么要通过A的实例呢?因为InnerAA中跟其它属性、方法一样,属于“对象属性”,必须通过类的实例才能访问。

firstpackage

A

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package firstpackage;

public class A{
    private String propA;
    public A(){};
    public A(String s){
        this.propA=s;
    }
    public void say(){
        InnerA innerA=new InnerA();
        innerA.say();
    }
    public class InnerA{
        public void say(){
            System.out.println("这是InnerA的say()方法,但是可以读取到外部类A的propA属性=====>"+propA);
        }
    }
}

secondpackage

TestB

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package secondpackage;

import firstpackage.A;

public class TestB {
    public static void main(String[] args) {
        A a=new A("一条来自secondpackage的信息");
        A.InnerA innerA=a.new InnerA();
        innerA.say();
    }
}

输出:“这是InnerA的say()方法,但是可以读取到外部类A的propA属性=====>一条来自secondpackage的信息”

1.2 protected、private内部类

这两个修饰符只能用在内部类上,普通类只能public或者不加修饰符(表示只能在同一个package内访问)。 跟属性、方法的修饰符一样,protected表示包内可以访问(也可以被子类访问,内部类基本不会这么用),private表示只能在该外部类内部访问到这个内部类,在其它地方通过外部类也无法访问到这个内部类。

可以把上面的例子中的TestB类放到跟类A同一个package下来验证protected的行为或者把上面的例子中的main方法放到类A中来验证private的行为。

1.3 方法中的内部类(local/局部/本地内部类)

如果内部类只在方法中用到一次,那么直接把类声明在这个方法中也是可以的。这里并不能用修饰符,因为方法作用域中的内容不能被外部访问,这比private的作用域还要严格。方法中的内部类还可以获取方法参数传递进来的变量,并保存为本地变量。

下面的例子中,通过say(String s)方法传递进来的字符串s,当方法结束后,变量s就消失了,但是InnerA在使用这个变量的时候保存在了本地,后续每一次事件触发时,都能正确的输出这个变量的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package firstpackage;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;


public class A{
    public static void main(String[] args) throws Exception{
        A a=new A("这是构造A的对象时传入的字符串");
        a.say("这是通过方法传递的字符串");
        //这里只是为了让程序保持运行
        Thread.sleep(5000);
        System.exit(0);
    }
    private String propA;
    public A(){};
    public A(String s){
        this.propA=s;
    }
    public void say(String s){
        class InnerA implements ActionListener{
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("这是内部类,但能获取外部类的属性========>"+propA+"\n还可以获取方法的参数并保存为本地变量========>"+s);
            }
        }
        Timer timer=new Timer(1000,new InnerA());
        timer.start();
    }
}

输出:

这是内部类,但能获取外部类的属性========>这是构造A的对象时传入的字符串 还可以获取方法的参数并保存为本地变量========>这是通过方法传递的字符串

这是内部类,但能获取外部类的属性========>这是构造A的对象时传入的字符串 还可以获取方法的参数并保存为本地变量========>这是通过方法传递的字符串

这是内部类,但能获取外部类的属性========>这是构造A的对象时传入的字符串 还可以获取方法的参数并保存为本地变量========>这是通过方法传递的字符串

这是内部类,但能获取外部类的属性========>这是构造A的对象时传入的字符串 还可以获取方法的参数并保存为本地变量========>这是通过方法传递的字符串

这是内部类,但能获取外部类的属性========>这是构造A的对象时传入的字符串 还可以获取方法的参数并保存为本地变量========>这是通过方法传递的字符串

1.4 匿名内部类

上面的例子,InnerA只是为了实现ActionListener接口,这种情况下,可以进一步用匿名内部类简化,连类名都不需要,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package firstpackage;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;


public class A{
    public static void main(String[] args) throws Exception{
        A a=new A("这是构造A的对象时传入的字符串");
        a.say("这是通过方法传递的字符串");
        //这里只是为了让程序保持运行
        Thread.sleep(5000);
        System.exit(0);
    }
    private String propA;
    public A(){};
    public A(String s){
        this.propA=s;
    }
    public void say(String s){
        Timer timer=new Timer(1000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("这是内部类,但能获取外部类的属性========>"+propA+"\n还可以获取方法的参数并保存为本地变量========>"+s);
            }
        });
        timer.start();
    }
}

后来,java引入lambda表达式进一步简化了这种写法,上面的say()方法可以改为:

1
2
3
4
5
6
public void say(String s){
    Timer timer=new Timer(1000, (ActionEvent e)->{
        System.out.println("这是内部类,但能获取外部类的属性========>"+propA+"\n还可以获取方法的参数并保存为本地变量========>"+s);
    });
    timer.start();
}

PS:lambda表达式只能用于代替实现FunctionalInterface接口的内部类,就是内部只有一个抽象方法的接口(default方法数量不限)

1.5 static内部类

static内部类和别的内部类有几点区别:

  • 因为是static,所以使用该类时不需要外部类的实例
  • 无法访问外部类的信息
  • 可以声明static的变量和方法

static内部类的作用类似package,只是让内部类更深了一层,可以让代码的结构更清晰,减少重名问题等,所以static内部类跟普通的类很像,很多地方称之为"嵌套类"。

关于static和其它内部类实例化的区别:跟类中的static属性一样,在外部类加载完成后,static内部类也加载完成了,所以如果要实例化static内部类,只要用new Outer.Inner()的表达方式就可以了。而普通的非static内部类,因为是定义在类中,属于"对象属性"级别,在外部类加载完成时,这些"对象属性"还不存在,等到外部类实例化时,内部类才作为这个外部类实例对象的"对象属性"加载进来,但此时只是有内部类完成,还没有实例化,需要使用外部类的对象来实例化内部类。

2.接口中的嵌套接口和嵌套类

接口中的类型成员,包括类和接口,都默认是public staic,也强制只能是public static

首先,接口不能被实例化,所以,任何非static的内容都毫无意义。至于publicJavaSE规范中规定,接口中的类型成员不是public就会在编译时报错。

关于这两个嵌套类型,举个例子,spring boot源码中有一个接口叫DeferredImportSelector,代码如下:

显示代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package org.springframework.context.annotation;

import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;

public interface DeferredImportSelector extends ImportSelector {

	@Nullable
	default Class<? extends Group> getImportGroup() {
		return null;
	}

	interface Group {

		void process(AnnotationMetadata metadata, DeferredImportSelector selector);

		Iterable<Entry> selectImports();

		class Entry {

			private final AnnotationMetadata metadata;

			private final String importClassName;

			public Entry(AnnotationMetadata metadata, String importClassName) {
				this.metadata = metadata;
				this.importClassName = importClassName;
			}

			public AnnotationMetadata getMetadata() {
				return this.metadata;
			}

			public String getImportClassName() {
				return this.importClassName;
			}

			@Override
			public boolean equals(@Nullable Object other) {
				if (this == other) {
					return true;
				}
				if (other == null || getClass() != other.getClass()) {
					return false;
				}
				Entry entry = (Entry) other;
				return (this.metadata.equals(entry.metadata) && this.importClassName.equals(entry.importClassName));
			}

			@Override
			public int hashCode() {
				return (this.metadata.hashCode() * 31 + this.importClassName.hashCode());
			}

			@Override
			public String toString() {
				return this.importClassName;
			}
		}
	}

}

DeferredImportSelector接口中有一个抽象方法getImportGroup,返回的是实现Group接口的对象的Class,Group是一个嵌套接口。通过IDE查看实现或者使用Group的地方,都跟DeferredImportSelector有关,所以定义在其内部作为嵌套接口比较直观、合理。虽然两者经常搭配使用,但从语法上来说,实现外部接口和它的嵌套接口没有必然联系,可以只实现其中的一个。

Group接口中还包含一个静态内部类EntryGroup接口中有个抽象方法selectImports返回的是Iterable<Entry>,Entry这个名称很普遍,放到内部类里可以避免重名,并且可以通过外部类的名称直观的看出这个类的作用。

3 总结

spring的源码中就经常使用内部类、内部接口等,可以让源码可读性更好,让外部类和内部类直接联系起来。总的来说,内部类和嵌套类、嵌套接口是为了提高程序代码的内聚性而存在的,也是封装思想的一种体现。