自己看了一眼oracle官网关于内部类的介绍, 不多, 但是有些点还是要注意的. 当然这些知识只能说是面试有用, 平时使用内部类, 如果违反规则, 编译器会提示你的, 所以看看就行, 没必要背熟.
先把我们用的名词说清楚.
我们说的内部类, 官方的叫法是嵌套类(Nested Classes), 嵌套类包括两种, 分别是静态嵌套类(Static Nested Classes)和内部类(Inner Classes), 其中内部类又有三种形式, 第一种就是我们常见的内部类, 其他两种特殊形式的内部类分别是局部类(Local Classes)和匿名类(Anonymous Classes).
下面分别介绍他们.
为了介绍方便, 我们统一使用内部类来称呼这些东西, 内部类分成静态内部类和非静态内部类, 非静态内部类有额外两种特殊形式, 一种叫局部类, 另一种叫匿名内部类. 同时, 我们把包裹内部类的类称为外围类.
内部类在作为外围类的成员时, 比如下面这种形式:
class OuterClass {
...
static class StaticNestedClass {
...
}
class InnerClass {
...
}
}
内部类可以用private
, protected
, public
或者package private
(什么都不写, 称之为package private, 也就是包权限)修饰.
回忆一下, 外围类只能使用public
或者package private
修饰.
静态内部类除了访问权限修饰符比外围类多以外, 和外围类没有区别, 只是代码上将静态内部类组织在了外围类里面. 如果在外围类外部引用静态内部类, 需要带上外围类的名字, 比如, 我想new一个静态内部类的实例:
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
注意, 虽然静态内部类代码是写在外围类里面的, 但是并不能访问外围类的非公开成员, 因为实际上它们就是两个不同的类, 不同的类之间当然不能访问对方的非公开成员.
可以这么理解, 虽然静态内部类代码写在外围类里面, 但它是在外围类的外面, 外围类对它来说仍然是一个黑盒.
我们这里讨论的是作为类成员存在的非静态内部类, 也就是形如:
class OuterClass {
class InnerClass {
...
}
}
非静态内部类和静态内部类不同, 非静态内部类能访问外围类的一切成员, 包括私有成员, 就好像它确实是在外围类的里面, 能看到外围类这个黑盒内部的细节.
而很容易被忽视的一点是, 外部类虽然不能直接访问内部类的成员, 但是可以通过内部类的实例访问, 注意, 外部类能访问非静态内部类私有成员.
非静态内部类实际上是和它的外围类实例相关联的, 换句话说, 它隐式持有一个外围类的引用, 虽然我们代码里面没写, 但是Java在生成字节码的时候会给非静态内部类添加一些人造的构造方法, 会使非静态内部类实例化时拿到外围类实例的引用并作为成员变量保存起来, 有兴趣的同学可以看下反编译出来的smali代码, 里面可以看到我说的这些.
注意非静态内部类是一定要和外围类相关联的, 也就是说, 只要有一个非静态内部类, 它就一定会持有一个外围类的实例引用, 它不能脱离外围类的实例存在, 在代码上表现出来是我们不能直接new一个非静态内部类. 即便是要直接new, 也必须先new一个外围类, 通过外围类来创建非静态内部类的实例, 像下面这样:
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
注意上面这行代码的new的写法是object.new
形式.
不止这个限制, 由于非静态内部类不能脱离外围类实例单独存在, 它不能有static成员. static成员是和类相关的, 不和实例相关, 而非静态内部类必须依赖一个实例才能存在, 所以它不能有static成员也是很容易理解的.
当然我这里说的static成员, 除了一种, 那就是static final
形式的常量. 很奇怪吧, 非静态内部类竟然允许定义常量, 事实确实是这样.
题外话: 说到常量的声明,
static final int a
和final int b
有什么区别呢? a是真正的常量, b应该叫不可变量, a这种定义形式不可能出现在方法体里, 而b这种形式可以. b如果定义在类成员中, 应该是每个实例都有一个b, 而a则是一个类仅有一个a.
非静态内部类里面不能定义接口, 因为接口是隐式static的.
另外静态初始化块也是不允许出现在非静态内部类中的.
定义在语句块里的非静态内部类叫做局部类. 通常来说, 我们在方法体里定义局部类.
有人问为什么没有静态内部类的局部类, Java方法体里面就没法定义static修饰的东西, 更别提静态内部类了.
题外话: C/C++里面倒是可以在方法体里面定义static变量, 那种变量可见性是方法体内, 但生命周期却超过了定义它的方法体.
下面这段代码展示了一个局部类PhoneNumber
:
/*
code from https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html
*/
public class LocalClassExample {
static String regularExpression = "[^0-9]";
public static void validatePhoneNumber(String phoneNumber1, String phoneNumber2) {
final int numberLength = 10;
// Valid in JDK 8 and later:
// int numberLength = 10;
class PhoneNumber {
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
// numberLength = 7;
String currentNumber = phoneNumber.replaceAll(regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
public String getNumber() {
return formattedPhoneNumber;
}
// Valid in JDK 8 and later:
// public void printOriginalNumbers() {
// System.out.println("Original numbers are " + phoneNumber1 +
// " and " + phoneNumber2);
// }
}
PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
// Valid in JDK 8 and later:
// myNumber1.printOriginalNumbers();
if (myNumber1.getNumber() == null)
System.out.println("First number is invalid");
else
System.out.println("First number is " + myNumber1.getNumber());
if (myNumber2.getNumber() == null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is " + myNumber2.getNumber());
}
public static void main(String... args) {
validatePhoneNumber("123-456-7890", "456-7890");
}
}
作为非静态内部类的一种特殊形式, 非静态内部类的所有限制对局部类同样成立.
局部类能访问外围类的所有成员, 此外, 由于它是定义在方法体里的, 它甚至可以访问方法体的局部变量, 但必须是final
修饰的局部变量.
题外话: 写过Android可能知道, 局部类/匿名类如果想访问方法的参数, 我们需要在方法参数上手动添加
final
从Java SE 8开始, 局部类不仅可以访问标记为final
的方法局部变量和方法参数, 还可以访问实际上是final
(effectively final)的的局部变量或方法参数, 什么叫effectively final呢, 就是说我们没有改变过它的值的变量或参数, 最简单的方法, 就是我们手动给它加上final
, 编译器不报错, 就说明它是effectively final的.
比如下面这段代码:
PhoneNumber(String phoneNumber) {
//numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
phoneNumber
就是effectively final的, 如果我们把赋值语句前面的注释去掉, 那它就不是effectively final了.
简单来讲就是Java SE 8开始, 允许局部类允许访问隐式final的局部变量或方法参数.
题外话: 方法体也里面不能定义接口, 因为接口是隐式static的.
匿名内部类可以看成是一种没有名字的局部类, 它可以让我们的类的定义和实例化同时进行.
下面代码中展示了局部类和匿名内部类的使用.
public class HelloWorldAnonymousClasses {
interface HelloWorld {
public void greet();
}
public void sayHello() {
class EnglishGreeting implements HelloWorld {
public void greet() {
greetSomeone("world");
}
}
HelloWorld englishGreeting = new EnglishGreeting();
HelloWorld spanishGreeting = new HelloWorld() {
public void greet() {
greetSomeone("mundo");
}
};
englishGreeting.greet();
spanishGreeting.greet();
}
public static void main(String... args) {
HelloWorldAnonymousClasses myApp =
new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
匿名内部类的定义和实例化往往是在一条语句里完成的, 所以我们会看到匿名内部类的最后面还会有一个分号.
匿名内部类的访问外部变量的规则和局部类相同.
匿名内部类不能定义构造方法, 这很容易理解, 因为连类名都没有, 构造方法连名字都不知道, 更别提定义了.
在非静态内部类中定义的变量会屏蔽外围的同名变量, 比如下面这段代码:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String... args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
输出是:
x = 23
this.x = 1
ShadowTest.this.x = 0
注意访问各个层次的同命变量的不同写法.
同时, 注意在局部类或匿名内部类的方法中, 如果把外围方法的局部变量屏蔽了, 就没法在当前方法中访问那个局部变量了.