本文将从内部类的定义、作用、分类、使用示例等方面,对内部类进行梳理,以期掌握内部类的使用。
内部类定义
在 Java 中,可以将一个类放在另一个类里面或者一个方法里面定义。这种定义在一个类的内部的另一个类称为内部类。内部类可以看做外部类的一个成员,所以内部类可以拥有private、default、protected、public等访问权限修饰,也可以用final、static等修饰符来修饰。(外部顶级类只能使用public和default,final等修饰,外部定级类不能用static修饰)。
需要说明的是,内部类是一个编译器现象,在字节码语言中,只有类的概念,没有外部类或内部类的概念。内部类定义示例如下:
// 外部类
public class OuterClass {
// 内部类
class InnerClass {
}
}
内部类作用
Java语言引入内部类,主要有以下两方面的考虑:
(1) 完善Java语言的多继承能力。Java语言是单继承机制(所有的子类拥有一个共同的父类),这种设计极大的简化了语言的使用,但是有些场景仍需要多继承机制。Java语言引入内部类,就是为了完善Java语言的多继承能力。每个内部类都能独立地继承一个类,无论外部类是否已经继承一个类,对于内部类都没有影响。在开发设计中,会存在一些使用接口很难解决的问题,而类却只能继承一个父类。这个时候可以利用内部类去继承其他父类和实现多个接口来解决。JDK中HashMap就使用了内部类,关键代码示例如下:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 为了让Key具备Set的能力,继承了AbstractSet
final class KeySet extends AbstractSet<K> {
// ...
}
// 为了让Value具备Collection的能力,继承了AbstractCollection
final class Values extends AbstractCollection<V> {
// ...
}
// ...
}
(2) 提高封装性。内部类可以根据访问权限实现对外部的不同程度的隐藏。这复合面向对象思想的高内聚、低耦合原则。将内部类隐藏在外部类中,也能提高代码的可读性和可维护性。
内部类的划分及使用
内部类的划分,可以参照类的成员变量的分类。内部类根据使用的修饰符不同和定义的位置不同,可进行如下划分:
(1) 定义在外部类的成员所在层级
成员内部类(没有static修饰符)
静态内部类(有static修饰符)
(2)定义在外部类的方法体/代码块内
局部内部类(有类名)
匿名内部类(没有类名)
成员内部类
在外部类的内部定义的非静态的内部类,叫成员内部类(Member Inner Class)。创建成员内部类的示例代码如下:
// OuterClass.java
public class OuterClass {
private String privateField;
String defaultField;
protected String protectedField;
public String publicField;
class DefaultInnerClass {
private String privateField;
String defaultField;
protected String protectedField;
public String publicField;
}
}
在外部类中声明一个成员内部类,可以看做是将该成员类当做成员变量或成员方法处理。如上述示例代码中的内部类,其访问权限是默认访问权限(包权限)。同时,也支持用private、protected、public、final、static等访问控制符或修饰符来修饰。注意,不建议用public修饰内部类。这是因为,这样做会破坏内部类用于隐藏细节的特性。错误示例如下:
// OuterClass.java
public class OuterClass {
private String privateField;
public class PublicInnerClass {
private String privateField;
String defaultField;
protected String protectedField;
public String publicField;
public void visit() {
// 注意这种写法,可以直接在内部类的方法中访问外部类实例的私有成员
System.out.println(OuterClass.this.privateField);
// 注意这种写法,可以创建新的外部类实例并访问其私有成员
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.privateField);
}
}
}
// 测试用例
public void testInnerClass() {
OuterClass outClassInstance = new OuterClass();
// 注意这种写法
OuterClass.PublicInnerClass innerClass = outClassInstance.new PublicInnerClass();
// 无法在外部访问非公有成员
// System.out.println(innerClass.protectedField);
System.out.println(innerClass.publicField);
}
再一次说明,尽管Java语法支持定义public修饰的内部类,但不建议这么做。因为这样与内部类设计初衷不符。如果真需要用public修饰,应该将其单独定义并通过组合的方式使用外部类。
在使用成员内部类时,需要特别注意内部类与外部类之间的访问。成员内部类可以直接访问外部类,外部类需通过引用方式访问内部类实例。示例代码如下:
// OuterClass.java
public class OuterClass {
private String privateField;
private void visit() {
// 实例化内部类后,可以访问内部类实例的私有成员
PrivateInnerClass privateInnerClass = new PrivateInnerClass();
System.out.println(privateInnerClass.privateField);
}
private class PrivateInnerClass {
private String privateField;
public void visit() {
// 内部类方法中,可以直接使用外部类的私有成员
System.out.println(OuterClass.this.privateField);
// 内部类方法中,可以实例化外部类,并访问外部类实例的私有成员
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.privateField);
}
}
}
需要说明的是,内部类在调用实例方法时,其外部类实例已创建完毕,这也是为什么可以在内部类的方法中访问外部类实例的成员的原因。
静态内部类
在外部类的内部定义的静态的内部类,叫静态内部类(Static Inner Class)。静态内部类也称为嵌套类(Nested Class),嵌套类特指静态内部类。创建静态内部类的示例代码如下:
// OuterClass.java
public class OuterClass {
private String privateField;
private static String privateStaticField;
static class StaticInnerClass {
private String privateField;
private static String privateStaticField;
}
}
在使用静态内部类时,同样关注静态内部类与外部类之间的访问。静态内部类可以直接访问外部类的静态成员或实例成员,外部类可以直接访问静态内部类的静态成员或通过引用方式访问内部类实例。示例代码如下:
public class OuterClass {
private String privateField;
private static String privateStaticField;
private void visit() {
// 外部类可以直接访问静态内部类的静态成员
System.out.println(StaticInnerClass.privateStaticField);
// 外部类可以实例化静态内部类,并访问该实例的私有成员
StaticInnerClass staticInnerClass = new StaticInnerClass();
System.out.println(StaticInnerClass.privateStaticField);
}
static class StaticInnerClass {
private String privateField;
private static String privateStaticField;
public static void visit() {
// 静态内部类方法中,可以直接使用外部类的静态私有成员
System.out.println(OuterClass.privateStaticField);
// 静态内部类方法中,可以实例化外部类,并访问外部类实例的私有成员
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.privateField);
}
}
}
静态内部类的经典使用场景是基于静态内部类实现单例模式,示例代码如下:
public class Singleton {
private Singleton() {
}
// 静态方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
使用静态内部类实现单例模式的优势是:外部类在加载时,并不会立即加载其静态内部类,而是在第一次调用静态内部时,才会初始化静态内部类。(这里是加载 SingleTonHolder 类)。这样做,不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
局部内部类
在外部类的方法中,定义的非静态的内部类,叫局部内部类(Local Inner Class)。创建局部内部类的示例代码如下:
// OuterClass.java
public class OuterClass {
private String privateField;
private void visitLocalInnerClass() {
class LocalInnerClass {
private String privateField;
}
LocalInnerClass localInnerClass = new LocalInnerClass();
System.out.println(localInnerClass.privateField);
}
}
在使用局部内部类时,也需要关注局部内部类与外部类之间的访问。局部内部类可以直接访问外部类的静态成员或实例成员,外部类则无法在方法体外访问该局部内部类(局部内部类定义在方法内,限制该内部类只能在该方法中生效)。示例代码如下:
private void visitLocalInnerClass() {
class LocalInnerClass {
private String privateField;
public void visit() {
System.out.println(outerClass.privateField);
// 局部内部类方法中,可以直接使用外部类的私有成员
System.out.println(OuterClass.this.privateField);
// 局部内部类方法中,可以实例化外部类,并访问外部类实例的私有成员
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.privateField);
}
}
// 外部类方法中,可以直接实例化局部内部类,并访问其私有成员
LocalInnerClass localInnerClass = new LocalInnerClass();
localInnerClass.visit();
System.out.println(localInnerClass.privateField);
}
直接使用局部内部类的场景并不多(笔者还没看到过相关的代码),更多的是使用Lamda表达式代替。对局部内部类仅做了解即可。
匿名内部类
在外部类的方法中,定义的非静态的没有类名的内部类,叫匿名内部类(Anonymous Inner Class)。创建匿名内部类的示例代码如下:
// OuterClass.java
public class OuterClass {
private String privateField;
private void visitAnonymousInnerClass() {
Runnable runnable = new Runnable() {
private String privateField;
@Override
public void run() {
System.out.println(privateField);
}
};
}
}
在使用匿名内部类时,同样需要关注匿名内部类与外部类之间的访问。匿名内部类是局部内部类的一种特性,所以遵循局部内部类与外部类的规则。这里再重复说明下:匿名内部类可以直接访问外部类的静态成员或实例成员,外部类则无法在方法体外访问匿名内部类(匿名内部类定义在方法内,限制该内部类只能在该方法中生效)。示例代码如下:
private void visitAnonymousInnerClass() {
// 外部类方法中,可以直接实例化匿名内部类,并访问其成员
Runnable runnable = new Runnable() {
private String privateField;
@Override
public void run() {
System.out.println(privateField);
// 匿名内部类方法中,可以直接使用外部类的私有成员
System.out.println(OuterClass.this.privateField);
// 匿名内部类方法中,可以实例化外部类,并访问外部类实例的私有成员
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.privateField);
}
};
runnable.run();
}
匿名内部类可以使用Lamda表达式简化,上述匿名内部类简化后的效果如下:
private void visitAnonymousInnerClass(String input) {
int localNumber = 0;
Runnable runnable = () -> {
System.out.println(localNumber);
System.out.println(input);
System.out.println(OuterClass.this.privateField);
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.privateField);
};
runnable.run();
}
匿名内部类适合只使用一次的局部内部类场景,当创建一个匿名内部类时,会立即创建该类的一个实例对象,匿名内部类因为没有类名,所以不能重复使用。
匿名内部类是局部内部类的特例(匿名的含义是编译器会自动给匿名内部类起一个名称)。与局部内部类一样,匿名内部类也有被Lamda表达式替代的风险。对于遗留代码还有部分保留匿名内部类写法,但是对于新增代码,尽量使用Lamda表达式。
总结
在 Java 中,将放在一个类的内部的另一个类称为内部类。根据使用的修饰符不同和定义的位置不同,可将内部类划分为:成员内部类(没有static修饰符),静态内部类(有static修饰符),局部内部类(有类名),匿名内部类(没有类名)四种。内部类的使用场景优先,且逐渐被Lamda表达式替代(由其是局部内部类、匿名内部类)。
参考
Java编程思想(第4版) Bruce Eckel 著 陈昊鹏译 第十章 内部类
https://www.w3schools.cn/java/java_inner_classes.asp Java 内部类 (嵌套类)
https://blog.csdn.net/liuxiao723846/article/details/108006609 Java内部类
https://blog.csdn.net/xiaojin21cen/article/details/104532199 单例模式之静态内部类
https://www.joshua317.com/article/212 Java内部类
https://www.runoob.com/java/java-inner-class.html Java内部类
https://developer.aliyun.com/article/726774 深入理解Java的内部类
https://www.cnblogs.com/shenjianeng/p/6409311.html java内部类使用总结
https://blog.csdn.net/liuxiao723846/article/details/108006609 Java内部类
https://segmentfault.com/a/1190000023832584 JAVA内部类的使用介绍