前言
本文简单介绍接口、lambda表达式、内部类等java编程基础知识,本文例子提供的比较多,篇幅比较大,设计接口定义、实现comparable接口与Comparator<T>接口以及二者区别,拷贝的系列问题和注意事项;lambda表达式的各种情况及其升级版:方法引用;内部类介绍了普通内部类、静态内部类、匿名内部类、局部内部类,并且对内部类进行了反编译处理来查看底层原来,简单解释内部类带来的内存泄漏问题
目录
7.Comparable接口和Comparator接口之间的差异
一、接口
1.接口概念
官方解释:Java接口是一系列方法的声明,是一些方法特征的集合。
我的看法: 接口是抽象类的更进一步,1000%纯度的抽象,是一组需求。正常情况下,Java无法实现一个类继承多个类,所以诞生一个接口来处理这个问题,一个类可以继承多个接口。
2.定义接口
接口中包含的方法都是抽象方法, 字段只能包含静态常量。一个类实现接口,就要把它内部的抽象方法给重写。
public class InterfaceDemo {
public static void main(String[] args) {
//可以使用接口类引用子类
Ieating cat = new Animal("fish" , "cat");
cat.eat();
}
}
//接口
interface Ieating{
public final static int age = 10;
void eat();
}
//实现接口
class Animal implements Ieating{
String food;
String name;
public Animal(String food,String name){
this.food = food;
this.name = name;
}
@Override
public void eat(){
System.out.println(this.name + "爱吃" + this.food);
}
}
- 使用interface定义一个接口
- 接口中的方法一定是抽象方法,因此可以省略abstract
- 接口中的方法一定是public,因此可以省略public
- Animal使用Implement继承接口,此时表达含义不再是“扩展”,而是实现接口
- 在调用的时候同样可以创建一个接口的引用,对应到一个子类的实例
- 接口不能被单独实例化
接口中只能包含抽象方法,对于字段来说,接口中只能包含静态常量(final static)
interface Ieating{
public final static int age = 10;
void eat();
}
其中的 public, static, fifinal 的关键字都可以省略. 省略后的 仍然表示 public 的静态常量
提示 :1. 我们创建接口的时候 , 接口的命名一般以大写字母 I 开头 .2. 接口的命名一般使用 " 形容词 " 词性的单词 .3. 阿里编码规范中约定 , 接口中的方法和属性不要加任何修饰符号 , 保持代码的简洁性
一个错误代码:
//接口
interface Ieating{
int age = 10;
void eat();
}
//实现接口
class Animal implements Ieating{
@Override
//由于默认为default权限,比接口内定义的eat()public 权限低,所以无法覆盖,出现错误
void eat(){
System.out.println(this.name + "爱吃" + this.food);
}
}
完整格式 | 简化 |
interface Ieating{ public final static int age = 10; public abstract void eat(); } | interface Ieating{ int age = 10; void eat(); } |
除此之外,接口内也可以有默认方法:
interface Ieating{
int age = 10;
void eat();
//默认实现
default int aa(){
.....
}
}
默认方法可以解决,接口增加非默认方法带来的“源代码兼容问题”和未重新编译直接调用的问题
3. 实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的 . 然而 Java 中只支持单继承 , 一个类只能 extends 一个父类 . 但是可以同时实现多个接口 , 也能达到多继承类似的效果 . 现在我们通过类来表示一组动物
动物类:
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
提供一组接口, 分别表示 "会飞的", "会跑的", "会游泳的”
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
创建几个具体的动物
猫会跑
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼会游
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
青蛙, 既能跑, 又能游(两栖动物)
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
还有一种神奇的动物, 水陆空三栖, 叫做 "鸭子"
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法 : 一个类继承一个父类 , 同时实现多种接口 .继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .
4.回调函数
回调的本质:A类a方法调用B类的b方法,b方法在调用A类的c方法
(1)同步回调
举个例子:班长问我“1+1”等于多少,我处理这个问题,然后给他提供反馈
public class CallbackDemo {
public static void main(String[] args) {
Monitor m = new Monitor();
m.weekJob();
}
}
//A类
class Monitor implements ICallBack{
@Override
//c方法
public void setResult(String result){
System.out.println(result);
}
//此处调用B类b方法
public void weekJob(){
System.out.println("班长发布了任务");
Follower f = new Follower();
//将A类引用传递给B类的d方法
f.doingSomething(this);
}
}
//B类
class Follower{
//d方法,调用A类c方法
public void doingSomething(ICallBack call){
call.setResult("做完啦~~");
}
}
interface ICallBack{
void setResult(String result);
}
//结果
班长发布了任务
做完啦~~
这里还涉及一个向上转型的问题,this传递给ICallBack,间接完成了下面这个代码:
ICallBack call = new Monitor();
(2)异步回调
同步回调是当下完成并上交反馈,而异步回调是忙完我手头上的东西后,来调用你A的c方法来给你提供回馈,并且在我忙的时候,班长也不应该傻傻等我忙完后在给他答案,而是应该去做他自己的事情,这个时候我们就需要使用线程来帮忙了
public class CallbackDemo2 {
public static void main(String[] args) {
Monitor2 m = new Monitor2();
m.weekJob();
System.out.println("班长卷去了");
}
}
//A类
class Monitor2 implements ICallBack2{
@Override
//c方法
public void setResult(String result){
System.out.println(result);
}
//此处调用B类b方法
public void weekJob(){
System.out.println("班长发布了任务");
Follower2 f = new Follower2();
//将A类引用传递给B类的d方法
f.doingSomething(this);
}
}
//B类
class Follower2{
//d方法,调用A类c方法
public void doingSomething(ICallBack2 call){
//此处创建一个线程
new Thread(()->{
//这个就是B要干的事情,忙完在执行班长的任务
for(int i = 0 ; i < 10 ; i++){
}
call.setResult("做完啦~~");
}).start();
}
}
interface ICallBack2{
void setResult(String result);
}
//结果
班长发布了任务
班长卷去了
做完啦~~
如果此处没有创建多一个线程,那么语句是一条一条执行的,为同步回调
5.类实现Comparable接口
类实现Comparable接口,并实现其中compareTo方法,有利于使用排序函数
例子:
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序)
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
};
数组有一个现成的sort方法,但是直接使用会报异常
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常.
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
java.lang.Comparable
其实不难想到,Arrays.sort()方法就是给数值服务的,它可以直接判断大小,但是类它没有方式去判断大小,所以需要我们额外指定,指定的方式就是类实现Comparable接口,并实现其中的compareTo方法
//实现接口
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
//重写toString()
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
//重写compareTo()方法
@Override
public int compareTo(Object o) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
- 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
- 如果当前对象和参数对象不分先后, 返回 0;
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项 : 对于 sort 方法来说 , 需要传入的数组的每个对象都是 " 可比较 " 的 , 需要具备 compareTo 这样的能力 . 通过重写 compareTo 方法的方式 , 就可以定义比较规则 .
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)
public static void sort(Comparable[] array) {
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1].compareTo(array[cur]) > 0) {
// 说明顺序不符合要求, 交换两个变量的位置
Comparable tmp = array[cur - 1];
array[cur - 1] = array[cur];
array[cur] = tmp;
}
}
}
}
再次执行代码
sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
6.实现comparator接口
上一节解释了如何对一个对象数组进行排序。现在我们来学习排序的第二个版本利用比较器Comparator<T>接口
查看Java API文档
这个sort方法要求我们提供一个类型的数组,然后提供一个比较器,然后这个方法以这个比较器为工具来进行排序,T就是指这个要排序的类型
但是我们知道,一个接口是不能被直接创建实例的,所以我们需要创建一个匿名接口的对象,然后通过接口类型变量来进行保存
Comparator<Integer> c = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
下面是实操例子:
public static void main(String[] args) {
//Integer包装器类数组
Integer[] arr = {1,2,3,4,8,7,9,2,2,4,2};
//比较器引用和重写
Comparator<Integer> c = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
//传递比较数组和重写比较器
Arrays.sort(arr , c);
for(Integer i : arr ){
System.out.println(i);
}
}
7.Comparable接口和Comparator接口之间的差异
二者都是完成一个辅助比较的任务,前者是一个类要去继承接口,来保证这个类能被sort()之间进行比较然后排序,后者则是在类没有实现Comparable接口的时候,自定义一个比较器来进行传参比较,前者多用于继承完成辅助任务,后者多用于自身创建匿名对象来完成任务
8.接口间的继承
接口可以继承一个接口, 达到复用的效果 . 使用 extends 关键字 .
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
9.Cloneable接口和深拷贝
1)标志接口
Cloneable接口是Java提供的少数标记接口之一。Comparable等接口的通常用途是确保一个类实现一个或一个组特点的方法。而与这些接口不一样的是,标记接口不包含任何方法,它的唯一作用就是允许在类型查询中使用instanceof。
所以由此可见,我们克隆使用的是从超类Object继承来的。如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查类型异常
2)实现Cloneable接口
Java 中内置了一些很有用的接口 , Clonable 就是其中之一 . Object 类中存在一个 clone 方法 , 调用这个方法可以创建一个对象的 " 拷贝 ". 但是要想合法调用 clone 方法, 必须要先 实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常.
public class CloneableDemo {
public static void main(String[] args) {
Animal animal = new Animal();
//调用克隆方法
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
//实现接口
class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
//创建一个Animal的空对象
Animal o = null;
try {
//这里调用Object的克隆方法克隆Object对象,然后强制类型转化
o = (Animal)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
// 输出结果
// false
在这里有一点值得注意, Object类是对Animal一无所知,所以只能通过逐个字段进行拷贝
3)Object类的clone()
先进行内容回顾:
protected修饰的方法,在不同包时候,子类可以访问该方法,但是不能在包外创建新的对象调用方法。
即protected权限成员可以被非同包的子类访问,指子类内部可以直接使用父类protected成员;而不是在外部创建子类对象,通过子类对象访问父类protected成员。可以到此网页【菜鸟教程】对protected进行更好的了解
Object类是在lang包下的,而它的clone()是一个protected方法,当在其他包下的时候,不能通过子类的创建来调用clone() ,但是可以被子类自身直接调用,例子如下:
public class ClonableDemo {
public static void main(String[] args) throws CloneNotSupportedException {
IAnimal animal = new IAnimal();
//报出异常
//'clone()' 在 'java.lang.Object' 中具有 protected 访问权限
IAnimal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
//IAnimal内部可以调用lang包下的Object的clone()
class IAnimal implements Cloneable {
private String name;
public IAnimal() throws CloneNotSupportedException{
IAnimal a = (IAnimal) super.clone();
}
}
所以我们必须立刻马上紧急重写clone() ,并将其的权限修改为public ,这样所以方法就都能对类的clone()进行调用
class IAnimal implements Cloneable {
private String name;
@Override
//修改权限为public
public IAnimal clone() throws CloneNotSupportedException {
......
}
}
4)浅拷贝VS深拷贝
Cloneable拷贝出的对象是一份“浅拷贝”
public class Test {
static class A implements Cloneable {
public int num = 0;
@Override
public A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
static class B implements Cloneable {
public A a = new A();
@Override
public B clone() throws CloneNotSupportedException {
return (B)super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
B b = new B();
B b2 = b.clone();
b.a.num = 10;
System.out.println(b2.a.num);
}
}
// 执行结果
10
如果想要实现深拷贝,这里就需要也对b内的a进行拷贝(当类多的时候,就需要进行套娃了)
static class A implements Cloneable {
public int num = 0;
@Override
public A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
static class B implements Cloneable {
public A a = new A();
@Override
public B clone() throws CloneNotSupportedException {
//将a进行拷贝并赋值
this.a = a.clone();
return (B)super.clone();
}
}
10.抽象类与接口的区别
核心区别: 抽象类中可以包含普通方法和普通字段 , 这样的普通方法和字段可以被子类直接使用 ( 不必重写 ), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法 .
11.接口使用注意事项:
1)解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或者另一个接口中定义同样 的方法。会出现二义性冲突
Java给出的规则是:
- 超类优先:如果一个类提供了一个具体方法,同名而且相同的参数类型的默认方法会被忽略
- 接口冲突:如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认方法)相同的参数方法,必须覆盖这个方法来解决冲突
二、lambda表达式
lambda表达式采用一种简洁的语法定义代码块,它是一个可以传递的代码块,可以在之后被执行一次或者多次
1.使用lamdba表达式好处
在上面接口内容时,有提到Comparator<T>接口,我们在使用它的时候,需要给它创建匿名对象,并且进行引用,事实上,真正起到作用的被重写的compare()方法,它才是被sort()方法一直重复调用的。由于代码段在Java中无法进行直接传递,所以诞生出来lambda表达式来解决这一问题。
2.lambda表达式语法
1)语法:
格式:() -> {}
- 左侧:指定了lambda表达式需要所有的参数
- 右侧:制定了lambda体,即lambda表达式要执行的功能
(parameters) -> expression
或
(parameters) ->{ statements; }
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但无参数或多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
以下是lambda表达是各种情况操作代码:
无参,无返回值,只有一条执行语句
实现Runnable接口
Thread a = new Thread(()-> System.out.println());
一个参数,无返回值
Consumer<String> consumer = (x)-> System.out.println(x);
多个参数,多条语句
Comparator<String> c = (o1, o2) -> {
System.out.println();
return o1.length() - o2.length();
};
多个参数,一条语句,有返回值,省略return
Comparator<String> c = (o1, o2) -> o1.length() - o2.length();
2)实例:
回到Comparetor<T>接口的例子:
public static void main(String[] args) {
Integer[] arr = {1,2,3,4,8,7,9,2,2,4,2};
Comparator<Integer> c = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
};
Arrays.sort(arr , c);
for(Integer i : arr ){
System.out.println(i);
}
}
下面利用lambda表达式实现上面Comparator<T> 接口例子
public static void main(String[] args) {
Integer[] arr = {1,2,3,4,8,7,9,2,2,4,2};
Arrays.sort(arr , (o1 , o2)-> o1 - o2);
for(Integer i : arr ){
System.out.println(i);
}
}
类型参数可以省略,JVM可以通过上下文来进行推断,在执行javac编译程序时,JVM根据程序上下文推断了参数的类型。lambda表达式依赖上下文环境
3.函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就也可以使用lambda表达式,,这种接口也被称为函数式接口,
我们自定义函数式接口时,要使@FunctionalInterface注解,这样可以用于检测它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是函数式接口
4.变量作用域(lambda使用注意事项)
lambda表达式只能引用标记了的final外层局部变量,意识就是说,lambda不能在内部修改这个变量的值,否则会出现编译错误
final static int num = 10;
public static void main(String[] args) {
IA a = ()-> System.out.println(num);
}
interface IA{
void print();
}
可以使用lambda表达式访问外层的局部变量
public static void main(String[] args) {
final int num = 10;
IA a = ()-> System.out.println(num);
}
interface IA{
void print();
}
lambda表达式可以引用没有被final修饰的变量,但是千万不能在内部进行修改(相当于心中无修改,调用自然神)
public static void main(String[] args) {
int num = 10;
IA a = ()-> System.out.println(num);
}
interface IA{
void print();
}
lambda表达式不允许声明一个局部变量同名参数或者局部变量
//错误示例
public static void main(String[] args) {
final int num = 10;
//报错,出现重名num
IA a = (num ,num1)-> System.out.println();
}
interface IA{
void print(int num1 , int num2);
}
//正确示例
public static void main(String[] args) {
final int num = 10;
IA a = (num1 ,num2)-> System.out.println();
}
interface IA{
void print(int num1 , int num2);
}
在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方式的this参数。
class Application{
public void init(){
ActionListener lister = (event)->{
System.out.println(this.toString());
........
}
}
}
表达式this.toString() 会调用Application 对象的toString() 方法,而不是ActionListener实例的方法,毕竟接口不属于类,也继承不了Object内部定义的toString()
5.方法引用
1)说明
有时,lambda表达式涉及一个方法。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
var timer = new Timer(1000 , (event) -> System.out.println(event));
但是,可以直接把println方法直接传递给Timer构造器
var timer = new Timer(1000 , System.out::println);
表达式System.out::println是一个方法引用,它指示编译器生成一个函数式的接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,它的actionPerformaed(ActionEvent e)方法的调用System.out.println(e).
2)使用
假设要对字符串进行排序,而不是考虑字母的大小。可以传递以下方式表达式:
Arrays.sort(Strings,String::comparaToIngnoreCase);
从这些例子可以得知,要运用 :: 运算符分隔方法名与对象或类名。主要是3种情况:
- object::instanceMethod
- Class::instanceMethod
- Class::staticMethod
第一种情况,方法引用等价于向方法传递参数lambda表达式。对于System.out::println,对象是System.out,所以方法表达式等价于:
(x)->System.out.println(x);
对于第二种情况,第1个参数会成为方法的隐式参数。例如,String::compareToIngoreCase等同于:
(x,y)->x.comparaToIngnoreCase(y)
对于第三种情况,所以参数都传递到静态方法:Math::pow等价于:
(x,y)->Math.pow(x , y)
注意,只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。考虑一下lambda表达式:
s->s.length() == 0
这里有一个方法调用。但是还有一个比较,所以这里不能使用方法引用
3)构造器引用
与方法引用一样,但是方法名为new
Class::new
对于存在多个构造器,编译器会自己根据上下文进行推倒,例如:int[]::new是一个构造器的引用,它有一个参数即数组长度。等价于:
(x)-> new int[x]
三、内部类
此节特别声明,以下内将内部类外面的一层统称为外部类
1.普通内部类
public class InnerClassDemo {
class test1{
}
}
普通的内部类依赖于外部类的存在而存在,也就是说,要想创建test1类就需要将InnerClassDemo进行创建
public class InnerClassDemo {
final int oField1= 4;
private int oField2 = 5;
public int oField3 = 6;
public InnerClassDemo(){
Test1 inner = new Test1();
System.out.println("其内部类field1字段值为:"+ inner.field1);
System.out.println("其内部类field2字段值为:"+ inner.field2);
System.out.println("其内部类field3字段值为:"+ inner.field3);
}
class Test1{
final int field1= 1;
private int field2 = 2;
public int field3 = 3;
public Test1(){
System.out.println("其外部类field1字段值为:" + oField1);
System.out.println("其外部类field2字段值为:" + oField2);
System.out.println("其外部类field3字段值为:" + oField3);
System.out.println("==============================");
}
}
public static void main(String[] args) {
new InnerClassDemo();
}
}
//结果
其外部类field1字段值为:4
其外部类field2字段值为:5
其外部类field3字段值为:6
==============================
其内部类field1字段值为:1
其内部类field2字段值为:2
其内部类field3字段值为:3
这里可以看出内部类可以访问外部类的字段,外部类也可以访问内部类的字段,后面我们会从底层研究这个问题
2.静态内部类
一个类的静态成员,不属于类的任何一个对象,只属于类本身,只要在能访问到这个类的地方,就能通过类名.静态成员进行访问,静态内部类也不例外,它独属于这个类本身,不依靠类的对象创建而存在
public class InnerClassDemo {
final int oField1= 4;
private int oField2 = 5;
public int oField3 = 6;
public InnerClassDemo(){
Test2 inner = new Test2();
System.out.println("其内部类field1字段值为:"+ inner.field1);
System.out.println("其内部类field2字段值为:"+ inner.field2);
System.out.println("其内部类field3字段值为:"+ inner.field3);
}
static class Test2{
final int field1= 1;
private int field2 = 2;
public int field3 = 3;
public Test2(){
// 编译错误,原因很简单,static存在比外部类早,故无法访问到字段
// System.out.println("其外部类field1字段值为:" + oField1);
System.out.println("我是" + Test2.class.getName() + "对象");
}
}
public static void main(String[] args) {
//创建静态内部类,不依赖于外部类而存在
// new InnerClassDemo.Test2();
new InnerClassDemo(1);
}
}
//结果
我是learner.InnerClassDemo$Test2对象
其内部类field1字段值为:1
其内部类field2字段值为:2
其内部类field3字段值为:3
还是一样外部类能访问到内部类的私有成员
3.匿名内部类
匿名类,我们在前面的接口和lambda表达式中有使用过,其手法就是直接new一个接口/类对象,对于接口,我们还需要对接口方法进行重写;对于类,我们直接new一个
public class InnerClassDemo {
final int oField1= 4;
private int oField2 = 5;
public int oField3 = 6;
//内部接口
interface Itest3{
void test();
}
class Test4{
public Test4(){
System.out.println("其外部类field1字段值为:" + oField1);
System.out.println("其外部类field2字段值为:" + oField2);
System.out.println("其外部类field3字段值为:" + oField3);
System.out.println("==============================");
}
}
public InnerClassDemo(){
//这个就是匿名内部类
new Itest3(){
@Override
public void test() {
}
};
//匿名内部类
new Test4();
//因为是匿名对象,外部类没办法获取到名字,所以无法访问到内部类的字段
}
public static void main(String[] args) {
//这个创建方式,InnerClassDemo对象也是匿名类
new InnerClassDemo();
}
}
//结果
其外部类field1字段值为:4
其外部类field2字段值为:5
其外部类field3字段值为:6
4.局部内部类
声明局部内部类不能有访问说明符(public 或者 private)。局部内部类的作业域被限定在声明这个局部类的块中
public class InnerClassDemo {
final int oField1= 4;
private int oField2 = 5;
public int oField3 = 6;
public InnerClassDemo(){
//局部内部类A
class A {
// static int field = 1; // 编译错误!局部内部类中不能定义 static 字段
public A() {
System.out.println("创建 " + A.class.getSimpleName() + " 对象");
System.out.println("其外部类的 field1 字段的值为: " + oField1);
System.out.println("其外部类的 field2 字段的值为: " + oField2);
System.out.println("其外部类的 field3 字段的值为: " + oField3);
}
}
A a = new A();
if (true) {
// 局部内部类 B,只能在当前代码块中使用
class B {
public B() {
System.out.println("创建 " + B.class.getSimpleName() + " 对象");
System.out.println("其外部类的 field1 字段的值为: " + oField1);
System.out.println("其外部类的 field2 字段的值为: " + oField2);
System.out.println("其外部类的 field3 字段的值为: " + oField3);
}
}
B b = new B();
}
}
public static void main(String[] args) {
new InnerClassDemo();
}
}
//结果
创建 A 对象
其外部类的 field1 字段的值为: 4
其外部类的 field2 字段的值为: 5
其外部类的 field3 字段的值为: 6
创建 B 对象
其外部类的 field1 字段的值为: 4
其外部类的 field2 字段的值为: 5
其外部类的 field3 字段的值为: 6
局部内部类也可以访问外部类的字段,但是外部类无法访问局部内部类的字段,原因在于,局部内部类只在局部有效,出了这个代码块就会失效,所以外部类根本拿不到字段
5.深入理解内部类
对于外部类的private字段和内部类的private,理应都只能被其本身调用到,而无法被其他类调用到,但是实事相反,二者都互相偷看到了对方的private字段。
《Java编程思想》给出来的说法是:内部类可以访问外部类任何成员,外部类在创建内部类的对象后,也可以访问内部类任何成员
《Java核心卷I》给的说法是:内部类保存了外部类的引用,而这个引用是无法被查看的
这里我们用编译的手段去研究这个问题,先创建一个简单的内外部类先:
public class InnerTest {
int field1 = 1;
private int field2 = 10;
public InnerTest(){
TestA a = new TestA();
int num = a.x2;
}
public class TestA{
int x1 = field1;
private int x2 = field2;
}
}
这里我们要对这个类进行编译与反编译操作:(基于java 16.0.1,有的jdk版本反编译不太一样)
编译:javac InnerTest.java
此时生成了两个.class文件,
反编译:javap -c innerTest.class 和javap -c innerTest$TestA.class
生成两个字节码文件
对于外部类,它在这个jdk版本是通过getfield方法来获取字段的,这个就有点像是反射而得到的内部类字段。
对于内部类,以这种特殊方式查看到了它对外部类的引用,并且使用这个引用查看并捕获到了field1和field2(getfield 与putfield)
特别说明,如果你是jdk 8 版本,它使用的并不是getField方法,而是一个叫做access的静态方法来获取各自的private字段,感兴趣可以去查一查试一试
6.内存泄漏
到这来相信你已经对内部类有一定的理解。基于此,可以简单说一下内部类引发的内存泄漏,内存泄漏其实就是内存被开辟了,但是没有回收掉,就像C语言中的malloc函数,分配内存,但是没有feel掉,就导致某个程序一直占有这块内存地址。虽然说Java比较幸运,有自己的自动回收垃圾内存机制,但是我们刚刚也学习过内部类,像非静态内部类都是依靠外部类而存在的内部类,当这个外部类的工作结束了,它的内部类还在继续,这样外部类所占用的那一块内存就没办法被回收,造成内存泄漏,相比之下,静态内部类是不依赖外部类存在的,所以外部类结束工作,内存回收对静态内部类基本没影响
感谢观看