什么是内部类?
通俗的讲,就是把一个类放在另一个类的内部定义,这个定义在其他类内部的类就叫做内部类,这个包含内部类的类就叫做外部类。即内部类是依附于外部类而存在的。
使用内部类有什么好处呢?
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,所以就不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访问外部类的私有数据,其实就是把内部类也当成一个外部类的成员,同一个类的成员之间自然可以互相访问。
- 使用匿名内部类创建那些仅需要一次使用的类会更方便,无需再去专门定义那些类。
内部类和外部类有什么区别?
- 内部类要定义在其他类里面。
- 内部类比外部类可以多使用三个修饰符:private、protected、static,外部类只能是public或默认包访问权限。
- 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号。
- 非静态内部类不能拥有静态成员。
内部类主要分为哪几类?
- 非静态内部类(也有称作成员内部类)
- 静态内部类(嵌套类)
- 局部内部类(方法内部类)
- 匿名内部类
关于这个分类,发现不同的书有不同的叫法,Java疯狂讲义是将非静态内部类和静态内部类统称成员内部类。而大多时候,我们说的成员内部类其实就是非静态内部类,静态内部类被单独称作嵌套类。不管名称如何,下面我们以粗体字名称为准来逐个分析~
非静态内部类
在一个类中直接定义的内部类,与普通的类成员没什么区别,可以与普通成员一样进行修饰和限制。非静态内部类不能含有static的变量和方法。
下面这个例子是在Cow类里定义了一个CowLeg的非静态内部类,并在CowLeg类的实例方法里直接访问了Cow的private实例变量。
//定义一个外部类
public class Cow {
private double weight;
//外部类的两个构造器
public Cow() {}
public Cow(double weight) {
this.weight = weight;
}
//定义一个非静态内部类
private class CowLeg {
//非静态内部类的两个实例变量
private double length;
private String color;
//非静态内部类的两个构造器
public CowLeg() {}
public CowLeg(double length, String color) {
this.length = length;
this.color = color;
}
//...省略length、color的setter和getter方法
//非静态内部类的实例方法
public void info() {
System.out.println("当前牛腿颜色是:" + color + ",高是:" + length);
//直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在奶牛重:" + weight);
}
}
//外部类的实例方法
public void test() {
CowLeg c1 = new CowLeg(1.12,"黑白相间");
c1.info();
}
public static void main(String[] args) {
Cow cow = new Cow(378.9);
cow.test();
}
}
编译后,看到文件所在路径生成了两个.class文件:
运行结果:
可以看出,非静态内部类CowLeg类确实直接访问了外部类Cow的private成员,那么这是为什么呢?
因为在非静态内部类对象里,保存了一个它所寄生的外部类对象的引用。当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在就到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在就使用该变量;如果不存在,则通过这个外部类对象的引用去外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,系统将出现编译错误,提示找不到该变量。
那么问题来了,如果外部类成员变量和内部类里方法的局部变量同名,该怎么办呢?
我们可以通过使用this、外部类类名.this作为区分。
我们说内部类成员可以访问外部类private成员,那反过来外部类能直接访问内部类private成员吗呢?答案是不成立的。一方面,非静态内部类成员只有在非静态内部类范围内是可知的,并不能被外部类直接使用。另一方面,当你创建了一个外部类对象并调用其实例方法时,非静态内部类对象根本是不存在的。如果外部类非要使用,则必须显式创建非静态内部类对象来调用访问其实例成员。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。比如下面这个例子,编译报错。
Java中也不允许在非静态内部类里定义静态成员。
public class InnerNoStatic {
private class InnerClass {
//下面三个静态声明都将引发编译错误
//非静态内部类里不能有静态声明
static {
System.out.println("=============");
} //静态初始化块
private static int inProp; //静态成员变量
private static void test() {} //静态方法
}
}
静态内部类
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此也成类内部类、嵌套类。
这个static关键字的作用是把类的成员变成类相关,而不是实例相关。只能用来修饰内部类,即将内部类变成外部类相关;不能用来修饰外部类,因为外部类的上一级程序单元是包。
静态内部类可以包含静态成员,也可以包含非静态成员。只能访问外部类的类成员,即静态成员。下面程序演示了这个规则:
public class StaticInnerClassTest {
private int pro1 = 5;
private static int pro2 = 9;
static class StaticInnerClass {
//静态内部类类可以包含静态成员
private static int age;
public void accessOuterProp() {
//下面代码出错,静态内部类无法访问外部类的实例变量
System.out.println(pro1);
//下面代码正确
System.out.println(pro2);
}
}
}
因为静态内部类是外部类的类相关的,而不是外部类的对象相关的。也就是说,静态内部类对象不是寄生在外部类的实例中,而是寄生在外部类的类本身中。当静态内部类对象存在时,并不存在一个被它寄生的外部类对象,静态内部类对象只持有外部类的引用,没有持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被寄生的外部类对象,这将引起错误。
所以我们也可以说静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以直接使用静态内部类对象作为调用者来访问静态内部类的实例成员。比如像下面这个例子一样:
public class AccessStaticInnerClass {
static class StaticInnerClass {
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp() {
System.out.println(StaticInnerClass.prop1);
System.out.println(new StaticInnerClass().prop2);
}
}
Java还允许在接口里定义内部类,接口里定义的内部类默认用public static修饰,也就是说,接口内部类只能是静态内部类。如果省略访问控制符,则该内部类默认是public。
使用内部类(非静态内部类和静态内部类)
在外部类内部使用内部类
与平常使用普通类没有太大区别,通过new调用内部类构造器来创建实例,一样可以直接通过内部类类名来定义变量。在外部类内部定义内部类的子类与平常定义子类也没有太大区别。
在外部类以外使用非静态内部类
class Out { //定义一个内部类,不使用访问控制符 //即只有同一个包中的其他类可访问该内部类 class In { public In(String msg) { System.out.println(msg); } } } public class CreateInnerInstance { public static void main(String[] args) { //简化写法 Out.In in = new Out().new In("test"); /* //定义内部类变量 Out.In in; //创建外部类实例,非静态内部类实例寄生在该实例中 Out out = new Out(); //通过外部类实例和new来调用内部类构造器创建非静态内部类实例 in = out.new In("test"); */ } } ******************************************************* //定义一个子类继承了Out类的非静态内部类In //该类不一样是内部类,它可以是一个外部类 public class SubClass extends Out.In { //显式定义SubClass的构造器 public SubClass(Out out) { //通过传入的Out对象显式调用In的构造器 out.super("hello"); } }
在外部类以外使用静态内部类
class StaticOut { //定义一个静态内部类,不适用访问控制符 //即同一个包中的其他类可以访问该内部类 static class StaticIn { public StaticIn() { System.out.println("静态内部类的构造器"); } } } public class CreateStaticInnerInstance { public static void main(String[] args) { StaticOut.StaticIn in = new StaticOut.StaticIn(); /* //定义内部类变量 StaticOut.StaticIn in; //通过new来调用内部类构造器创建静态内部类实例 in = new StaticOut.StaticIn(); */ } }
相比之下,可以看出,使用静态内部类比使用非静态内部类要简单,只要把外部类当成静态内部类的包空间即可。所以有限考虑使用静态内部类。
局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅对该方法里有效。由于局部内部类不能放在外部类的方法以外使用,因此局部内部类不能使用访问控制符和static修饰符。
public class LocalInnerClass {
public static void main(String[] args) {
//定义局部内部类
class InnerBase {
int a;
}
//定义局部内部类的子类
class InnerSub extends InnerBase {
int b;
}
//创建局部内部类的对象
InnerSub is = new InnerSub();
is.a = 5;
is.b = 8;
System.out.println("InnerSub对象的a和b实例变量是:" + is.a + "," + is.b);
}
}
编译后生成了三个.class:
发现比成员内部类的class文件的文件名多了一个数字,因为同一个类里不可能有两个同名的成员内部类,而可能有两个以上同名的局部内部类(处于不同方法中),所以Java为局部内部类的class文件名中增加一个数字用于区分。
实际中很少定义局部内部类,因为作用域太小了,只能在当前方法中使用。
匿名内部类(呼…终于到最后一个了鸭
匿名内部类适合创建那种只需要一次使用的类。创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
定义匿名内部类格式如下:
new 实现接口() | 父类构造器(实参列表) {
//匿名内部类的类体
}
从这个格式中可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。
关于匿名内部类还有如下两条规则:
- 匿名内部类不是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
- 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下例子:
interface Product {
public double getPrice();
public String getName();
}
public class AnonymousTest {
public void test(Product p) {
System.out.println("购买了一个" + p.getName() + ",花掉了" + p.getPrice());
}
public static void main(String[] args) {
AnonymousTest ta = new AnonymousTest();
//调用test()方法时,需要传入一个Product参数
//此处传入其匿名实现类的实例
ta.test(new Product() {
@Override
public double getPrice() {
return 567.8;
}
@Override
public String getName() {
return "AGP显卡";
}
});
}
}
从上面的程序可以发现,匿名内部类无须class,而是在定义匿名内部类时直接生成该匿名内部类的对象。
由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有抽象方法。
当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。
但通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,这个“相似”指的是拥有相同的形参列表。
//创建抽象父类
abstract class Device {
private String name;
public abstract double getPrince();
public Device() {
}
public Device(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class AnonymousInner {
public void test(Device d) {
System.out.println("购买了一个" + d.getName() + ",花掉了" + d.getPrince());
}
public static void main(String[] args) {
AnonymousInner ai = new AnonymousInner();
//调用有参数的构造器创建Device匿名实现类的对象
ai.test(new Device("电子示波器") {
@Override
public double getPrince() {
return 67.8;
}
});
//调用无参数的构造器创建Device匿名实现类的对象
Device d = new Device() {
//初始化块
{
System.out.println("匿名内部类的初始化块...");
}
//实现抽象方法
@Override
public double getPrince() {
return 56.2;
}
//重写父类的实例方法
@Override
public String getName() {
return "键盘";
}
};
ai.test(d);
}
}
从Java8开始,如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰。
Java8将这个功能称为“effectively final”,它的意思是对于被匿名内部类访问的局部变量,可用final修饰也可不用,但必须按照有final修饰的方式来用——也就是一次赋值之后,就不能再重新赋值。
关于内部类的总结就是以上这些,可能还有地方描述的不够准确,但是对这块儿有了更深刻的认识。emm…博客还是要写的,不然真的容易忘啊(泪奔T.T