把一个类定义在另一个类内部,就是内部类(也叫嵌套类)。包含内部类的类称为外部类(也叫宿主类)。内部类的好处在于:
- 更好的封装。内部类隐藏在外部类之内,同一个包内的其他类就没法访问了;
- 内部类的成员可以直接访问外部类的private成员(变量、方法);外部类也可以直接访问内部类的private成员;
- 一些比较简单的类,可能只会被调用一次。这时不需要独立地定义这个类,而是把它们嵌入到外部类里。
大部分情况下,把内部类看成是成员内部类,也就是和成员变量、成员方法、构造器、初始化块类似的东西(想一想能够放在Java类内部的五种东西)。因此,成员内部类也就有两种:static修饰的“类内部类”(太拗口了,所以还是遵循传统叫静态内部类,或者就直接叫static修饰的成员内部类)和普通内部类。除此之外,成员内部类做为外部类的组成部分,具有访问权限,即public,private,protected。
外部类的上一级程序单元是包,所以它只有两个作用域:同一个包内和任意位置。因此,外部类只有两个访问权限:包访问权限,也就是
default或者直接缺省;和公开访问权限,也就是public。内部类则具有完整的4种访问权限。
成员内部类(普通内部类)
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;
}
// 省略get和set方法
private void info()
{
System.out.println("奶牛的重量:"+weight); // 直接访问外部类的私有成员
}
}
public void test()
{
var c1 = new CowLeg(1.12, "黑白相间");
System.out.println(c1.length);
c1.info(); // 直接访问内部类的私有成员变量和方法
}
public static void main(String args[])
{
var cow = new Cow(378.9);
cow.test();
}
}
上述代码中,外部类Cow嵌套了一个内部类CowLeg。在编译之后,会生成两个class文件:一个是Cow.class,另一个则是CowCowLeg.class。内部类编译后总是以‘外部类CowLeg.class。内部类编译后总是以`外部类CowLeg.class。内部类编译后总是以‘外部类内部类.class`形式存在。
内部类CowLeg之所以能够访问外部类的私有成员,是因为在非静态的内部类对象中,保存了一个所寄生的外部类对象的引用。
非静态内部类实例必须寄生在外部类实例中。
上述代码的内存分配情况如下图:

但是,反过来,外部类访问内部类就不成立了。外部类想要访问非静态的内部类,需要显式实例化内部类,如第34行。
一个非静态内部类对象的存在,肯定有一个被寄生的外部类对象。
但是一个外部类对象存在时,其非静态内部类对象并不一定存在!
此外,还要遵循静态成员不能访问非静态成员的规则,外部类的类方法、类代码块不能访问非静态内部类。
静态内部类(类内部类)
使用static修饰内部类以后,内部类就变成“类内部类”,说白了,就属于外部类本身,而不属于外部类的某个具体对象了。
同样,遵循静态成员不能访问非静态成员的规则,类内部类不能访问外部类的实例变量。
一个类内部类存在时,一定存在外部类本身。但是,外部类的实例并不一定存在。所以,在类内部类去试图访问外部类的实例变量或实例方法,就会引起错误。
反过来,外部类的所有方法、所有初始化块都可以使用类内部类。外部类在使用类内部类的类成员时,通过类内部类名.成员来调用;使用类内部类的实例成员时,通过实例化类内部类对象来调用:
public class AccessStaticInnerClass
{
static class StaticInnerClass
{
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp()
{
// System.out.println(prop1);
// 错误,不能直接访问
// 通过类内部类名访问类成员变量
System.out.println(StaticInnerClass.prop1);
// 通过内部类的对象访问实例变量
System.out.println(new StaticInnerClass().prop2);
}
}
Java还允许接口里定义内部类,默认使用public static修饰。
同样,Java还允许接口里定义接口,内部接口默认也是使用public static修饰的。
Java还允许类内定义接口。内部接口默认都是static的,后续的枚举也是,默认static。
在外部类之外使用内部类
private修饰的内部类只能在外部类中使用。若想在外部类之外使用非静态成员类,需要把内部类的访问权限设为:
public,在任何地方访问;protected,外部类同一个包的其他类和外部类的子类访问;default,外部类同一个包的其他类。
由于非静态成员类依赖外部类对象,因此在外部类之外使用时,首先要实例化一个外部类,然后再实例化该非静态成员类,最后再进行调用:
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("测试信息");
// 上述代码有点抽象,实际上是:
// Out.in in;
// Out out = new Out();
// in = out.new In("测试信息");
}
}
在外部类之外使用类内部类相对简单一点,这时外部类相当于一个包名。实例化的时候,不需要依赖外部类实例:
class StaticOut
{
static class StaticIn
{
public StaticIn()
{
System.out.println("静态内部类的构造器");
}
}
}
public class CreateStaticInnerInstance
{
public static void main(String[] args)
{
var in = new StaticOut.Staticin();
}
}
局部内部类
如果在方法中定义一个内部类,这个类就是局部内部类了。局部内部类只在这个方法内有效。
局部内部类只在方法内有效,所以使用static修饰没有意义。而且除了该方法没有别的地方能访问它,所以也不需要访问控制符。
public class LocalInnerClass
{
public static void main(String[] args)
{
// 局部内部类
class InnerBase
{
int a;
}
class InnerSub extends InnerBase
{
int b;
}
var is = new InnerSub();
is.a = 5;
is.b = 8;
}
}
编译上述文件,生成LocalInnerClass.class,LocalInnerClass$1InnerBase.class和LocalInnerClass$1InnerSub.class三个文件。可以发现规律:局部内部类的class文件在内部类的基础上,加了一位数字。这个数字是用来区别同名局部内部类的。
成员内部类是不可能重名的。但是局部内部类,由于位于不同的方法中,是可能重名的。
局部内部类是非鸡肋,代码冗长,功能有限。不建议使用。
匿名类
为了改进局部内部类的冗长性,对那种创建之后只使用一次的类,用匿名类更好。匿名类的特点是定义的同时就实例化了,从作用上看类似于赋值语句,是一次性的。
匿名类的语法如下:
new 实现接口() | 父类构造器(实参列表)
{
// 匿名类的类体
}
-
匿名类必须继承一个接口,或者一个父类。但最多继承一个。
-
由于匿名类在定义的时候就要实例化(new),所以不能是抽象类。
-
匿名类没有类名,所以没有办法定义构造器,但是可以用实例初始化块完成一些构造器要做的事情。
先看一个继承了接口的匿名类,这也是最常见的匿名类使用场景:
interface Product
{
double getPrice();
String getName();
}
public class AnonymousTest
{
public void test(Product p)
{
System.out.println("购买了一个"+p.getName()
+ ",花掉了"+p.getPrice());
}
public static void main(String[] args)
{
var ta = new AnonymousTest();
// 在调用test方法时,需要传入Product引用变量
// 这时可以使用匿名类实现接口Product,同时实例化一个匿名类对象,传入test方法
ta.test(new Product() // 这里对应匿名类的语法:new 实现接口()
{ // 类体
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP显卡";
}
});
}
}
由于接口不能直接实例化,正常情况下,需要定义一个类来实现(implement)这个接口,然后实例化这个类,让接口引用变量指向这个对象。“古典写法”应该是这样的:
class AnonymousProduct implements Product
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP显卡";
}
}
ta.test(new AnonymousProduct());
上述定义的AnonymousProduct类既可以是普通类,也可以是内部类,也可以是局部类。但无论如何,其代码要比使用匿名类复杂一些。
匿名类属于为了代码简洁而设计的“炫技式”写法,但是确实使得代码简洁美观了。
上述匿名类不能接受参数传入,本质原因是接口不接受参数。要想让匿名类接受参数,那就让它继承一个类。此时,匿名类因为能够执行父类的带参数的构造器,从而接受外部传参。但请注意,匿名类是没有自己的构造器的,因此参数传入匿名类后,需要在实例初始化块中进行处理。
下面看一个继承类的匿名类:
abstract class Device
{
private String name;
public abstract double getPrice();
public Device();
public Device(String name)
{
this.name = name;
}
// 省略name的set和get方法
}
public class AnonymousInner
{
public void test(Device d)
{
System.out.println("购买了一个"+p.getName()
+ ",花掉了"+p.getPrice());
}
public static void main(String[] args)
{
var ai = new AnonymousInner();
ai.test(new Device("电子示波器") // 匿名类继承类,调用父类的带参数构造器
{
// getName是父类的方法
// getPrice是重写父类的抽象方法
public double getPrice()
{
return 67.8;
}
});
var d = new Device() // 抽象类本来无法实例化
{ // 利用匿名类继承父类,并重写其中的抽象方法,从而可以实例化
// 没有构造器,只有初始化块
{
System.out.println("匿名内部类的初始化块");
}
// 实现父类的抽象方法
public double getPrice()
{
return 56.2;
}
// 可以重写父类的实例方法
public String getName()
{
return "键盘";
}
}
ai.test(d);
}
}
1109

被折叠的 条评论
为什么被折叠?



