内部类
内部类是指在一个外部类的内部再定义一个类。内部类的出现,再次打破了Java单继承的局限性。
内部类可以是静态 static 的,也可用 public,default,protected 和 private 修饰。(而外部顶级类即类名和文件名相同的只能使用 public 和 default)。
注意:内部类是一个编译时的概念,一旦编译成功,就会成为完全不同的两个类。
对于一个名为outer的外部类和其内部定义的名为inner的内部类。编译完成后出现 outer.class 和outer$inner.class 两类。所以内部类的成员变量/方法名是可以和外部类的相同的。
1 成员内部类
也就是普通的内部类,它在外部类里面一层。
成员内部类可以访问外部类的所有成员和方法;
外部类要访问成员内部类的成员和方法需要通过实例对象来访问;
成员内部类不能含有 static 的变量和方法。
事实上,这种写法编译器就能提示错误:
具体的使用方法,参考如下代码及注释:
public class Outer{
private int outi = 0;//外部类的成员
//外部类的方法
public void outPrint(){
System.out.println("out");
}
//成员内部类
class Inner{
int ini = outi + 1;//内部类可以访问外部类的成员变量
public void inPrint(){
outPrint();//内部类可以访问外部类的成员方法
System.out.println("in");
}
}
public static void main(String[] args) {
Outer outer = new Outer();
//这里因为main是静态方法,访问非静态的 Inner对象需要一个Outer对象
Inner inner = outer.new Inner();
inner.inPrint();//外部类访问内部类的成员方法
System.out.println(inner.ini);//外部类访问内部类的成员变量
}
}
2 静态内部类
解决普通的成员内部类不能含有 static 成员的问题,就是将成员内部类声明为 static 。
可以这么说:静态内部类只能访问其外围类的静态成员,除此之外与非静态内部类没有任何区别。
静态内部类相当于一个逻辑上可以独立出去的类,不过放在了内部而已。
- 静态内部类不依赖于外部类的加载。(根据是否使用去加载,相当于内外平行的关系。)
- 静态内部类不能直接访问外部类的非静态成员。(因为外部类加载的时候非静态成员是没有加载的,除非实例化之后)
对比普通成员内部类的代码如下:
public class Outer{
private int outi = 0;//外部类的成员
//外部类的方法
public void outPrint(){
System.out.println("out");
}
//静态成员内部类
static class Inner{
Outer outer = new Outer();
int ini = outer.outi+1;//只能通过实例
//int ini = outi + 1;//不能访问外部类的普通成员变量
public void inPrint(){
outer.outPrint();//只能通过实例
//outPrint();//不能访问外部类的普通成员方法
System.out.println("in");
}
}
public static void main(String[] args) {
Inner inner = new Inner();//可以直接new出来,因为相当于是两个独立的类
inner.inPrint();
System.out.println(inner.ini);
}
}
对于静态内部类来说,我们的感受会更加明显,既然两个类都没什么关系了,为什么还要放在内部作为一个内部类呢?
其实就是为了某一些层级关系,当 Inner 的使用范围很小,适用于包含在 Outer 类当中,但又不依赖于外在的类,就这么写,同时还能够让代码的类间层级关系更加清晰,而不用重新放在另一个文件。
3 局部内部类
局部内部类,是指内部类定义在方法内部,或者某一个作用域里的类。
- 局部内部类只能在方法里面实例化,外面就不行了;
- 局部内部类访问外部方法的变量,这个变量必须有 final 修饰。
例如下面的代码:
class Outter{
public void outMethod(){
final int i=0;
class Inner{
//使用i
}
Inner in=new Inner();
}
}
很明显,和普通的成员内部类是一样的定义,但是这里强调的特殊点:i 为什么必须是 final 的呢?
原因是:
- 当 JVM 运行到需要创建 Inner 对象之后,Outter 类已经全部运行完毕,那么垃圾回收机制是有可能释放掉局部变量 i 的,而如果按照普通的成员内部类的定义,i 应该可以访问才对,那这个问题需要一个约束,所以编译器解决了这个问题,他会在 Inner 类中创建了一个 i 的备份;
- 那么就会有新的问题,外部类的 i 变化的时候,内部类的 i 要一致才行;
- 因此不得不规定死这些局部域必须是常量,必须用 final 修饰,保证数据的一致。
4 匿名内部类
4.1 匿名内部类
当一个局部内部类没有名字,这就是匿名内部类。
- 实现的方式是建立一个带内容的外部类、或者接口的子类匿名对象。
- 和普通的局部内部类又不同,访问外部方法的变量,这个变量可以不是final修饰。(jdk1.8之后)
比如我们常见的两种
public static void main(String[] args) {
//第一种方式
new Thread(){
@Override
public void run() {
System.out.println("内部类输出……");
}
}.start();
//第二种方式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("内部类输出……");
}
}).start();
}
- 第一种方式就是,建立了一个带内容的外部类,重写了它的方法,但是这个类我们没有起名字,而是直接创建了一个实例;
- 第二种方式就是通过实现一个接口,实现对应的方法,同样,这个实现类我们也没有起名字,而是直接创建了一个实例。
正是因为上面所说的,匿名内部类可以访问外部的变量,而且不用是 final ,同理也可以持有外部类的引用,这种情况都可能会导致内存泄漏问题。
解决方案就是:
- 使用静态内部类,并且不要持有外部类的引用,如果要调用外部类方法或使用外部类属性,可以使用弱引用(前面说过,静态内部类是和外部类平行的,弱引用会安全)。
4.2 匿名内部类和 lambda 表达式
从 java 8 开始,引入了 Lambda 表达式,将代码块作为参数使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
仍然用上面的匿名内部类的代码作为示例:
public static void main(String[] args) {
new Thread(()-> System.out.println("内部类输出")).start();
}
可以看出,lambda 表达式代替匿名内部类的时候,lambda 代码块写的是代替实现抽象类的方法体,总结一下lambda表达式的语法主要由三部分构成:
形参列表,如果只有一个参数可以省略括号,当无参数类型时可以使用()或者obj来代替。
箭头 ->
代码块部分,如果代码只有一行则可以省略掉花括号,不然使用花括号将lambda表达式的代码部分标记出来。
写法方面,我们再自己定义一个接口,然后写一下 lambda 式有参数的情况:
//自定义接口
interface Origin{
int sum(int a, int b);//待实现方法,有参数
}
public class LambdaTest {
public static void main(String[] args) {
//写法1:使用lambda表达式实现接口
Origin o = (int a, int b)-> {
return a+b;
};
//写法2:省略参数类型
Origin o1 = (a, b)->{
return a+b;
};
//写法3,省略花括号(只适用于方法实现只有一行的情况)
Origin o2 = (a, b)-> a+b;
System.out.println(o.sum(100,100));
}
}
4.3 java 的四种引用类型
强引用:
最常见的普通对象引用,只要还有强引用指向一个对象,说明那个对象还活着,垃圾回收不会回收这种对象;
弱引用:
垃圾回收器一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收他的内存。(即使弱引用被其他强引用引用,还是会被回收)
软引用:
如果一个对象只具备软引用,如果内存空间足够,那么 JVM 就不会 GC 它,如果内存空间不足了,就会GC该对象。
虚引用:
如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被JVM当作垃圾进行GC。
不会被回收的情况?
强引用,只要还存在强引用就不会被回收。
关于强引用和弱引用带来的问题, 最明显的就在 ThreadLocal 的使用上。