文章目录
内部类
内部类的定义
在Java语言中类可以嵌套定义,广义的内部类指的是定义在另一个类当中的一个类
eg:
public class Demo{
}
class Computer{
private int a;
private class Cpu{
}
}
说明:
- 对于CPU这样的类,定义在一个类的类体中 —> 内部类
- 对象Computer这样的类,包裹这一个内部类 ----> 外部类
- 像Demo这样的类 ---------------------------------------> 外部其他类
分类
- 根据定义的位置不同:
- 定义在成员位置:成员内部类、静态内部类
- 定义在局部位置:局部内部类、匿名内部类、lambda表达式
成员内部类
位置:定义在成员位置
语法:权限修饰符 class 类名{ 类体}
权限修饰符
4种都可以
成员特点
不能有static
的声名
可以有全局常量
继承和实现
可以继承和实现外部的类或者接口
还可以继承成员内部类
访问特点
外部类和成员内部类的关系:把成员内部类当作外部类的一个成员看待
----->
:表示访问
- 内部类 -----> 外部类
- 可以通过成员名直接访问,内部类持有外部类对象的引用
- 如果出现内部类外部类同名的成员,怎么区分?
this.
访问的是内部类成员,外部类.this.
访问的是外部类成员 - 不受权限控制
- 外部类 -----> 内部类
- 成员方法:创建内部类对象,通过
对象.
访问 - 静态方法:创建内部类对象,通过
对象.
访问 - 不受权限控制
- 成员方法:创建内部类对象,通过
- 内部类 -----> 外部其他类
- 创建外部其他类对象,通过
对象.
访问 - 受权限控制
- 创建外部其他类对象,通过
- 外部其他类 -----> 内部类
- 先创建外部类对象,再创建内部类对象
eg:Outer.Inner inner = new Outer().Inner();
- 受权限控制
- 先创建外部类对象,再创建内部类对象
静态内部类
位置:成员位置
语法:在成员内部类前面加static
权限修饰符
4种都可以
成员特点
普通类有的都有
继承和实现
可以继承和实现外部的类或者接口
还可以继承静态内部类,成员内部类不可以
访问特点
外部类和静态内部类的关系:两个独立的类,静态内部类不依赖于外部类
----->
:表示访问
- 内部类 -----> 外部类
- 普通方法:创建外部类对象,通过
对象.
访问 - 静态方法:创建外部类对象,通过
对象.
访问 - 不受权限控制
- 普通方法:创建外部类对象,通过
- 外部类 -----> 内部类
- 普通成员方法:创建内部类对象,通过
对象.
访问 - 静态成员方法:创建内部类对象,通过
对象.
访问 - 不受权限控制
- 普通成员方法:创建内部类对象,通过
- 内部类 -----> 外部其他类
- 创建外部其他类对象,通过
对象.
访问 - 受权限控制
- 创建外部其他类对象,通过
- 外部其他类 -----> 内部类
- 先创建外部类对象,再创建内部类对象
eg:Outer.Inner inner = new Outer().Inner();
- 受权限控制
- 先创建外部类对象,再创建内部类对象
静态内部类和外部类之间的类加载,它们会相互影响吗?
答:是不会的,静态内部类和外部类之间本身就没有依赖关系,它们的类加载、new对象,都是没有关系的
局部内部类
定义:局部内部类是定义在一个方法或者一个作用域里面的类,简单来说,将局部内部类看成是局部变量即可,该类的有效范围仅在作用域内部。(这意味着要创建它的对象,必须在作用域内部创建)
位置:局部位置
语法:class 类名 { 类体}
权限修饰符
不需要了,没有意义,只在作用域内有效
成员特点
不能有静态static
声明
可以有全局常量、构造方法、构造代码块
继承和实现
可以继承和实现外部的类或者接口(重要作用)
访问特点
外部类和局部内部类的关系:把局部内部类当作外部类的一个局部变量理解
----->
:表示访问
- 内部类 -----> 外部类
- 成员方法:
- 可以直接通过
成员名
去访问; - 通过
this.
; - 通过
类名.this.
访问的外部类成员
- 可以直接通过
- 静态方法:需要先创建外部类对象,通过
对象.
访问 - 不受权限控制
- 成员方法:
- 外部类 -----> 内部类
- 成员方法:在作用域内创建局部内部类对象,通过
对象.
访问 - 静态方法:在作用域内创建局部内部类对象,通过
对象.
访问 - 不受权限控制
- 成员方法:在作用域内创建局部内部类对象,通过
- 内部类 -----> 外部其他类
- 创建外部其他类对象,通过
对象.
访问 - 受权限控制
- 创建外部其他类对象,通过
使用场景
- 方法需要返回一个对象,返回值类型是引用数据类型时。
- 方法需要返回一个对象时,可以在方法的局部位置写一个 局部内部类
- 继承/实现外部的类/接口,创建对象后作为返回值。
- 这是因为方法的返回值可以是返回值类型的对象,也可以是子类对象(当返回值类型是引用数据类型时)。
eg:
public static IA m1(){
class A implements IA{
}
return new A();
}
interface IA{
}
-
方法需要传入一个对象,形参数据类型是引用数据类型时。
- 方法需要传入一个对象实参时,可以在调用方法的位置定义一个 局部内部类
- 来继承/实现外部的类/接口,创建对象后作为方法的实参传入。
- 这是因为方法的实参可以是形参类型的对象,也可以子类对象(当返回值类型是引用数据类型时)。
eg:
public static void main(String[] args){
class Son extends Father{
}
m3(new Son());
}
public static void m3(Father father){}
优缺点
优点
- 绝对对外界隐藏,封装。
- 相比较于传统的定义类,然后创建对象,它相对更加简洁省事。
缺点
这个类是一次性的。
注意事项
-
-
在局部内部类的成员方法中,如果想要访问作用域内部的局部变量,那么该变量:
-
要么直接就用
final
修饰,是一个局部常量。 -
要么就是一个事实上的常量,即只能赋值一次,相当于用
final
修饰它。在Java8之前版本的JDK中,如果不给能在局部内部类内部,访问的局部变量直接加上final修饰,会编译报错。
原因? -----------------> 局部变量,和对象的生命周期冲突
-
-
-
- 局部内部类对象作为方法的返回值,返回值类型必须是其父类型
原因:作用域的问题
- 局部内部类对象作为方法的返回值,返回值类型必须是其父类型
内部类的优缺点和使用场景
- 场景一:无条件地访问外部类的所有元素(优点)
- 无论是成员内部类、静态内部类、局部内部类还是匿名内部类都可以无条件访问包裹着它的外部类的成员。
- 场景二:隐藏类
- 可以用
private
、protected
修饰内部类。 private
修饰内部类、外界感受不到该类存在。
- 可以用
- 场景三:实现多继承
- 可以创建多个成员内部类继承外部多个类
- 然后创建内部类对象,实际上就是外部类继承了多个类的成员
- 场景四:通过匿名内部类来优化简单的接口实现/Lambda表达式更简洁
- 重点:内部类要说使用频率 最高的肯定是匿名内部类和Lambda表达式
内部类的缺点
-
内部类的缺点也是显而易见,语法很复杂,在类中定义内部类也会导致类的结构变复杂,影响代码可读性。
-
除此之外,不合理使用内部类还可能导致内存泄漏(了解)
持有外部类对象引用的内部类对象,如果始终被使用而没有释放:
那么连带着外部类对象,也不会被销毁和释放内存。
这在极端的场景下,会导致堆内存溢出,存在一定的风险。
匿名内部类
匿名内部类,指的就是这个内部类没有名字。当然成员内部类和静态内部类没法没有名字,所以 匿名内部类指的是"没有名字的局部内部类"。
是一种特殊的局部内部类
语法
// 局部位置
new 类名或者接口名(){
// 某类名或者接口名的子类的类体
};
// 局部位置
解释说明:
new
表示创建对象,表示创建语法中"类名/接口名"的子类对象。- 这个语法结构,我们只知道这是一个子类对象,但这个子类到底叫啥,不知道,所以它是一个匿名类。当然匿名内部类更准确的说法,应该叫匿名内部类对象。
- 匿名内部类的本质是,一个继承了类或者实现了接口的匿名子类对象。
使用方式
- 可以直接在后面调用方法,访问它的成员(当一个匿名对象使用)
- 优点是:可以访问子类独有的成员。方便快捷,不需要用引用接收再去使用
- 缺点是:一次性,仅能用一次
eg:
new IA(){
@Override
public void m2(){
System.out.println("IA");
}
}.m2();
- 可以用(父)引用接收对象,然后再用引用访问成员
- 优点是:可以用多次
- 缺点是:不能访问子类独有成员(被父类引用限制了访问范围,无法强转,因为子类已经匿名了)
eg:
IA ia = new IA(){
@Override
public void m2(){
System.out.println("IA");
}
};
ia.m2();
ia.m2();
综上,两种使用方式场景不同。如果有多次使用需求,就需要父引用接收;反之如果仅用一次,或者需要访问子类独有成员,就必须直接使用,不能用引用接收。
使用场景
匿名内部类经常使用在以下两个场景中:
-
方法需要返回一个对象,返回值类型是引用数据类型时。
方法需要返回一个对象时,可以在方法的局部位置写一个 基于X类或X接口的匿名内部类对象 ,然后直接作为返回值返回给外界。
eg:
public static Father func1(){
return new Father(){
@Override
void m2(){
System.out.println("IAAAA");
};
}
-
方法需要传入一个对象,形参数据类型是引用数据类型时。
方法需要传入一个对象实参时,可以在调用方法的实参位置填入一个 基于X类或X接口的匿名内部类对象,就将它作为一个实参传给方法使用。
public static void main(String[] args){
func4(new IA(){
@Override
void m2(){
System.out.println("IAAAA");
};
}
优缺点
使用匿名内部类的优点:
- 绝对对外界隐藏,封装。
- 比起局部内部类,它更方便简洁了。所以实际开发中,匿名内部类基本取代了局部内部类的作用。
使用匿名内部类的缺点:
- 这个对象是一次性的。
总之,酌情使用匿名内部类对象,可以简化代码书写,方便省事。但不要为了使用而使用,假如存在多个场景都需要一个子类对象,那还是直接创建一个子类出来好了。
案例
问题:不修改main
方法和接口Inter
的前提下,补齐Outer
类代码,要求在控制台输出HelloWorld
几种实现方式:
- 手写类实现接口
- 局部内部类
- 匿名内部类
- lambda表达式
public class Demo{
public static void main(String[] args) {
Outer.method().show();
}
}
interface Inter {
void show();
}
class Outer {
// 1. 手写类实现接口
public static Inter method(){
return new InterImpl();
// 2. 局部内部类
class Inner implements Inter{
@Override
public void show(){
System.out.println("HelloWorld");
}
}
return new Inner();
// 3. 匿名内部类
return new Inner(){
@Override
public void show(){
System.out.println("HelloWorld");
}
}
// 4. lambda表达式
return () -> System.out.println("HelloWorld");
}
}
// 1. 手写类实现接口
class InerImpl implements Inter{
@Override
public void show(){
System.out.println("HelloWorld");
}
}
Lambda表达式
概述
- Lambda表达式仍然是局部内部类,是特殊的局部内部类,仍然定义在局部位置。
而且局部内部类的注意事项,也一样存在。 - Lambda表达式在取代匿名内部类时,不是全部都取代,
而是取代接口的匿名内部类,而类的匿名内部类Lambda表达式是不能取代的。 - Lambda表达式是匿名内部类的更进一步, Lambda表达式得到的也不是一个类,
而是一个对象,并且是接口的子类对象。
使用前提
-
Lambda表达式虽然说是取代接口的匿名内部类,但也不是什么接口都能用Lambda表达式创建子类对象。
-
Lambda表达式要求的接口中,必须有且仅有一个必须要实现的抽象方法。这种接口在Java中,被称之为功能接口。
-
功能接口在语法上,可以使用注解
@FunctionalInterface
标记在接口头上,用于检测一个接口是否是功能接口。
功能接口指的是,有且仅有一个必须要子类实现的抽象方法的接口。
@FunctionalInterface
interface IA{
void m1();
}
两个问题
- 功能接口中只能有一个方法吗?
- 功能接口中只能有一个抽象方法吗?
答:
-
不是,Java8中的默认方法和静态方法不需要子类实现,功能接口中可以允许有它们存在。
-
不是,有极个别比较特殊的抽象方法,可以不需要子类实现。
注:Object类是Java每一个类的父类,所以Object类当中的方法实现就可以作为接口抽象方法的实现。比如:
功能接口不仅有一个抽象方法
@FunctionalInterface interface IA{ void test(); boolean equals(Object obj); }
接口IA
仍然是一个功能方法,因为抽象方法boolean equals(Object obj);
可以直接使用Object类中的实现,无需子类实现。
lambda 语法
(形参列表) -> {
// 方法体
}
说明:
(形参列表)
表示功能接口中,必须要重写的抽象方法的形参列表。->
由一个英文横杠 + 英文大于号字符组成,它是Lambda表达式的运算符,读作goes to。{ //方法体 }
表示功能接口中,必须要重写的抽象方法的,方法体实现。
Lambda表达式的类型判断
我们需要帮助编译器,明确Lambda表达式所表示的对象的类型
常见的三种方法
-
直接用父接口引用接收
由于Lambda表达式表示的子类对象并没有自己独有的成员,所以直接用父类引用接收完全不会有任何问题。 -
不用引用接收,但是要直接告诉编译器Lambda表达式是哪个接口的子类对象,语法上有点像强转(但不是)。
语法:
((父接口的名字)Lambda表达式).方法名(实参)
这种方式有点类似于匿名对象,所以必须直接调用方法,不然会编译语法报错。
-
借助方法完成类型推断。
- 可以借助方法的返回值数据类型完成类型推断,因为方法的返回值编译器已经知道该返回什么对象。
- 可以借助方法的形参的数据类型完成类型推断,因为方法的实参编译器已经知道该传入什么对象。
举例:
public class Demo {
public static void main(String[] args) {
// 1. 使用父接口进行接收
IA ia = () -> {
System.out.println("方法1");
};
ia.m1();
// 2. 类似强制类型转换,没有使用引用接受
((IA) () -> {
System.out.println("方法2");
}).m1();
func2(() -> {
System.out.println("方法3");
});
}
// 3. 借助方法完成类型推断
// a. 可以借助方法的返回值数据类型完成类型推断,因为方法的返回值编译器已经知道该返回什么对象。
public static IA func1() {
// 使用lambda
return () -> {
System.out.println("方法3");
};
}
// b. 可以借助方法的形参的数据类型完成类型推断,因为方法的实参编译器已经知道该传入什么对象。
public static void func2(IA ia){
ia.m1();
}
// 用于检测一个接口是不是功能接口
@FunctionalInterface
interface IA{
void m1();
}
}
Lambda 表达式的简化操作
逐个部分简化:
-
(形参列表)
能不能简化呢?是可以的,因为功能接口中有且仅有一个必须要实现的抽象方法,那么:-
形参的数据类型是可以省略的,因为方法已经固定死了,形参一定是那些,不需要写出来。但形参的名字是不可省略的(因为要在方法体中使用)
-
特殊情况下:
- 如果形参列表中的形参只有一个,那么
()
小括号,也是可以省略的。 - 但是如果形参为空,()小括号是不可以省略的。
- 如果形参列表中的形参只有一个,那么
-
-
{ //方法体 }
方法体能不能简化呢?当然是可以的:- 如果方法重写的方法体只有一条语句的话,那么可以省略大括号。(类似于if/for省略大括号)
- 特殊的,如果只有一条语句且这条语句是返回值语句,那么大括号和return可以一起省略。
-
实际上在多数情况下,都不太可能一句话把方法体写完。多数情况下,Lambda表达式的抽象方法实现都会很复杂,那这样Lambda表达式就会写的很复杂,这就很难受了。而Lambda表达式,本质上就是重写了一个抽象方法的子类对象,所以Java允许Lambda表达式的抽象方法的实现可以直接指向一个已经存在的方法,而不是自己书写实现。这种语法在Java中称之为"方法引用"!
public class Demo1 {
public static void main(String[] args) {
INoReturnTwoParam iNoReturnTwoParam = (int a, int b) -> {
System.out.println(a + b);
};
iNoReturnTwoParam.test(1, 2);
// 1. 简化写法:形参列表的数据类型可以省略
INoReturnTwoParam iNoReturnTwoParam1 = (a, b) -> {
System.out.println(a);
System.out.println(b);
};
System.out.println("----------------------------");
INoReturnOneParam iNoReturnOneParam = (int a) -> {
System.out.println(a++);
};
// 简化写法
INoReturnOneParam iNoReturnOneParam1 = a -> {
System.out.println(a++);
};
iNoReturnOneParam.test(10);
System.out.println("----------------------------");
// 2. 如果形参为空,括号是不能省略掉的
INoReturnNoParam iNoReturnNoParam = () -> {
System.out.println("test");
};
iNoReturnNoParam.test();
System.out.println("----------------------------");
IHasReturnTwoParam iHasReturnTwoParam2 =
(a, b) -> {
int result = a + b;
return result;
};
int result = iHasReturnTwoParam2.test(1, 2);
System.out.println(result);
// 3. 简化写法
// 如果方法重写的方法体只有一条语句的话,那么可以省略大括号。(类似于if/for省略大括号)
// 如果只有一条语句且这条语句是返回值语句,那么大括号和return可以一起省略
IHasReturnTwoParam iHasReturnTwoParam3 =
(a, b) -> a + b;
INoReturnNoParam ip = () -> System.out.println("1111");
}
//无返回值无参数的功能接口
@FunctionalInterface
interface INoReturnNoParam {
void test();
}
//无返回值有一个参数的功能接口
@FunctionalInterface
interface INoReturnOneParam {
void test(int a);
}
//无返回值两个参数的功能接口
@FunctionalInterface
interface INoReturnTwoParam {
void test(int a, int b);
}
//有返回值无参数的功能接口
@FunctionalInterface
interface IHasReturnNoParam {
int test();
}
//有返回值一个参数的功能接口
@FunctionalInterface
interface IHasReturnOneParam {
int method(int a);
}
//有返回值两个参数的功能接口
@FunctionalInterface
interface IHasReturnTwoParam {
int test(int a, int b);
}
}
方法引用
Lambda表达式的主体只有1条语句时, 程序不仅可以省略主体的大括号, 还可以通过英文双冒号::
的语法来引用方法, 进一步简化Lambda表达式的书写.
什么样的方法,能够作为方法引用指向的功能接口中抽象方法的实现?只看三点:
- 返回值类型必须一致。
- (方法签名method signature一致)形参列表中的数量,类型,位置必须都对应上,形参名字无所谓。
- 方法的名字无所谓。
subString
它是String类中的成员方法
语法:subString(String s, int start, int end) [start, end)
eg:
String s = "abcde";
String substring = s.subString(1,3); // bc
举例
public class Demo3 {
public static void main(String[] args) {
// 1. 静态方法引用
// lambda写法
IA ia = () -> A.func1();
ia.testA();
IA ia1 = A::func1;
ia1.testA();
IB ib = s -> System.out.println(s);
ib.testB("aaaa");
// 2. 对象名引用成员方法
// Lambda写法
IC ic = a -> new C().func2(a);
ic.testC(100);
IC ic1 = new C()::func2;
ic1.testC(200);
// 3. 类名引用成员方法
ID id = (s, start, end) -> s.substring(start, end);
id.testD("abcdef", 1, 3);
ID id1 = String::substring;
String str = id1.testD("abcdef", 1, 3);
System.out.println(str);
// 4. 构造方法引用
IE ie = (a, b) -> new E(a, b);
ie.testE(1, 2);
IE ie1 = E::new;
ie1.testE(1, 3);
}
}
@FunctionalInterface
interface IA {
void testA();
}
@FunctionalInterface
interface IB {
void testB(String s);
}
@FunctionalInterface
interface IC {
void testC(int a);
}
@FunctionalInterface
interface ID {
String testD(String s, int start, int end);
}
@FunctionalInterface
interface IE {
void testE(int a, int b);
}
class A {
// 定义一个静态方法 作为IA接口中的testA方法的实现
static void func1() {
System.out.println("IA");
}
}
class C {
void func2(int m) {
System.out.println(m);
}
}
class E {
int a;
int b;
public E(int a, int b) {
this.a = a;
this.b = b;
}
}
Lambda表达式优缺点
1. 优点:
-
极大得简化了代码,使代码变得更加优雅。
-
函数式编程的代表,可能是未来高端的编程趋势
- Lambda表达式在Stream API中,配合集合类去使用,代码非常优雅和简洁,并且高效,实际开发中十分常用。
Stream API代码
list. stream(). filter(stu -> stu.getAge() >= 18). map(Student::getScore). forEach(System.out::println);
注:该Stream API完成,将学生对象集合中的,所有大于等于18岁的学生的成绩输出的工作。
2. 缺点:
- 过于简单的Lambda表达式,显然可读性很低。
- 过于简洁也意味着不容易Debug。
- 语法难度不低,熟练使用需要时间锻炼。