08. 面向对象设计
抽象类
学习目标:
- 掌握抽象类的定义
- 掌握抽象类的特点与使用
引例
以下猫狗继承案例,是一个非常基础的继承语法的代码。思考一下Animal类的继承体系,有什么问题吗?
从以下两个角度分析:
- 从代码角度上,有没有冗余代码?
- 从设计角度上,在这个案例中,会存在一个动物对象吗?
class Animal {
public void shout() {
System.out.println("动物叫");
}
}
class Cat extends Animal {
@Override
public void shout() {
System.out.println("猫叫");
}
}
class Dog extends Animal {
@Override
public void shout() {
System.out.println("狗叫");
}
}
分析一下
单纯从代码角度上,有没有冗余代码?
实际上是有的。从方法调用的角度考虑,Animal类当中的shout方法从来没有被调用过。该方法单纯就是让子类继承并重写的,它实际上没有被调用的需求。从最理想的角度上来说,它的方法体是可以去掉的,仅保留一个声明让子类去重写就可以了。
当然代码多几行少几行并不重要,重要是:从设计角度出发,Animal类是一个抽象的概念,用于指代所有动物。程序中根本不需要创建它的对象。而且对于一个抽象的概念,也不应该、也不可能去描述它的行为。
综上,我们提出两个改进的方向:
- Animal类就是作为顶层抽象祖先类而存在的,不需要创建对象,于是干脆不要创建对象了。
- Animal类当中的shout方法不需要方法体,仅用于作为被继承和重写一个方法声明。
当父类的某些方法, 需要声明, 但是又不确定如何实现时, 可以将其声明为抽象方法, 那么这个类就是抽象类
定义
按照上述构想我们首先把方法的方法体去掉,这样就得到一个抽象方法。它的语法是:
[修饰符列表] abstract 返回值类型 方法名(形参列表);
注:
- 抽象方法没有方法体,只有方法的声明(方法头)。但是不要忘记写分号";"。
- 抽象方法必须写在一个抽象类中。
接下来,我们定义抽象类。它的语法是:
[修饰符列表] abstract class 类名{
//类体
}
通过测试,我们发现抽象类不能创建对象,但抽象类可以继承,存在子类。并且抽象类作为父类,多态现象仍然存在。
在继承体系中,从祖先类开始,随着一个个子类的定义,子类变得越来越具体,而祖先类则更具有一般性和抽象性。在这种情况下,为了体现祖先类在设计上的抽象性,我们只将该类作为派生其他类的父类,而不能创建对象实例。这个时候,这样的类,我们称之为抽象类。

上层的类越来越抽象,下层的类越来越具体。
特点与使用
特点
抽象类的特点我们主要从以下角度:
- 类名命名特点是什么?
- 能用哪些修饰符?
- 成员特点
- 普通成员
- 静态成员
- 抽象方法
- 构造器
- 代码块
测试以后,结论如下:
抽象类仍然是一个类,它的命名仍然需要遵循大驼峰的命名规范。当然,它比较特殊,许多公司的开发规范中,会要求将抽象类命名为
AbstracXxx
或BaseXxx
,用于表示该类是一个抽象类。这是一个不错的命名习惯,推荐使用,但不强制。类class在定义时,本身可以使用的修饰符就比较少,除了访问权限修饰符之外,几乎就只有final、abstract等少数修饰符。但是但是final和abstract在修饰类时是冲突的,不能放在一起使用。final是阻止继承而abstract是抽象类,如果不继承抽象类没有任何意义。
抽象类的成员特点:
成员变量。抽象类可以定义普通类中能够定义的所有成员变量,在这一点上,和普通类是一样的。
成员方法。抽象类可以定义普通类中能够定义的所有成员方法,包括普通成员方法和静态成员方法,在这一点上,和普通类仍然是一样的
注:抽象类虽然不能创建对象,但是它有子类,这些具体实现方法可以让子类继承,子类可以选择直接使用,也可以选择重写,选择的权力在子类手上(假如一个方法在抽象类的多个子类中都需要被使用,那么就可以提取到抽象类中作为一个默认实现)
综上,不能发现: 普通类中能够定义的成员,在抽象类中都是可以定义的。
抽象方法。首先抽象类可以没有抽象方法,但是如果一个抽象类没有抽象方法,那么它就没有必要声明为抽象,直接声明为普通类就够了。抽象方法最重要的作用是起到 "占位" 的作用,任何普通类继承了该抽象类,都必须实现该抽象方法(强制的)
如果子类不想实现抽象父类中的抽象方法,可以把子类也设置为抽象类,抽象类就不需要实现父类中的抽象方法了。
构造器。构造器除了可以自己用,创建对象时给成员变量赋值,还可以给子类用,在子类对象初始化过程中,可以调用父类的构造方法给父类的成员变量赋值。所以抽象类虽然不能创建对象,但它也是有构造器的。实际上,在Java中,任何一个类都有构造器。
代码块,无论是构造代码块,还是静态代码块,都有。
综上:抽象类实际上和普通类能够定义的成员是一样的,普通类中有的东西它都有,只不过多了一个抽象方法。在不考虑创建对象的情况下,直接把一个类声明为abstract,是不会编译报错的。
使用
抽象类的使用,我们主要探讨,它的子类特点。
抽象类的子类可以有两种情况:
- 子类是普通类,普通类继承抽象类,必须实现所有的抽象方法。
- 子类是抽象类,抽象子类不需要实现抽象方法,抽象子类可以自由选择实现(或实现或不实现)。
注:抽象类还可以继承一个普通类, 实际上如果一个抽象类, 它没有直接父类, 那它就直接继承Object
注意事项
abstract关键字,不能用来修饰构造器、属性、代码块等结构。只能修饰类或者方法。
abstract修饰类时,类的修饰符不能有final。
abstract修饰方法时,该方法不能有修饰符:
- private
- static
- final
一个抽象类中可以没有抽象方法,但是没有意义
练习
- 抽象类编码练习
公司正在开发一套员工管理系统,需要对公司内部存在的员工进行管理
公司将员工分为三类,普通员工负责杂项工作,程序员负责技术开发,经理负责统筹规划
普通员工包含3个属性:姓名、工号以及工资,行为是工作
程序员包含3个属性:姓名、工号以及工资,行为是工作和加班(overtime work)
经理包含4个属性:姓名、工号、工资以及奖金(bonus),行为是工作
- 请使用所学的知识设计该员工系统
- 要求类中提供必要的方法进行属性访问(description)
类图结构

- 抽象类的作用是:做为继承体系中的顶层父类、祖先类而存在的。说白了所有继承了该抽象类的对象,都应该是一个该抽象类的对象。比如我定义了一个抽象的Person类,那么Student、Teacher等继承它是没问题的,但是让一个Cat继承就不行了(即便有成员可以复用)。
// 抽象的员工类
abstract class BaseStaff {
private String name;
private int id;
private double salary;
public BaseStaff(String name, int id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
public abstract void work();
// 定义打印属性的方法,子类可以选择直接使用,也可以选择重写
public void printStaff() {
System.out.println("id是" + id + "的员工,姓名是" + name + ",Ta的工资是" + salary);
}
public String getName() {
return name;
}
public int getId() {
return id;
}
public double getSalary() {
return salary;
}
}
class NormalStaff extends BaseStaff {
public NormalStaff(String name, int id, double salary) {
super(name, id, salary);
}
@Override
public void work() {
System.out.println("负责杂项工作");
}
}
class Coder extends BaseStaff {
public Coder(String name, int id, double salary) {
super(name, id, salary);
}
@Override
public void work() {
System.out.println("负责技术开发");
}
public void overTimeWork() {
System.out.println("996福报,蹭公司电,蹭公司网!");
}
}
class Manager extends BaseStaff {
double bonus;
public Manager(String name, int id, double salary, double bonus) {
super(name, id, salary);
this.bonus = bonus;
}
@Override
public void work() {
System.out.println("负责统筹规划");
}
@Override
public void printStaff() {
System.out.println("id是" + getId() + "的员工,姓名是" + getName() + ",Ta的工资是" + (getSalary() + bonus));
}
}
接口
学习目标:
- 掌握接口的定义
- 掌握接口的特点与使用
引例
在之前的猫狗案例中,我们将Animal类设置为抽象类,继承体系代码如下:
猫狗案例代码
abstract class Animal {
public abstract void shout();
}
class Cat extends Animal {
@Override
public void shout() {
System.out.println("猫叫");
}
}
class Dog extends Animal {
@Override
public void shout() {
System.out.println("狗叫");
}
}
现在我们有了新需求:一部分猫和狗,经过了特殊训练,能够直立行走了。那么这群特殊的猫和狗,怎么描述它们呢?
分析:
部分猫和狗经过特殊训练,才能够直立行走。所以不可能直接加个方法到Cat类和Dog类中,不是所有猫狗都有这个行为。而特殊的猫狗仍然是猫和狗,所以它们肯定还要继承猫和狗这两个类。可能很多同学,这时候想到的就是:重新定义两个新的猫狗类(比如超级狗和猫)继承猫狗,然后给出直立行走的方法就算完事了。这样做不是不行,需求是实现了,但还有一些小问题:
- 一方面没有体现代码复用。
- 更重要的是,没有体现出特殊训练的过程。假如还有一些动物也要直立行走,增加这个行为,如何体现出规范性、标准性呢?体现出它们是特殊的经过训练的动物呢?
这里我们想到了抽象类,因为抽象类中抽象方法,可以强制约束子类实现它,这样就是一种标准和规范,体现了它经过了特殊训练,和之前不同了。
于是我们定义抽象类,给出抽象方法,让超级猫狗类去继承这个抽象类,可行吗?
理想很美好,现实很骨感。显然做不到,因为Java不支持多继承。除开这点这么做也不是完美的,问题是:
- 这个抽象类里面是对功能和行为的抽象,缺少属性(单独一个直立行走的行为是一个什么对象?)所以单独的行为不应该被抽象为类,不符合类的定义。
- 继承这个抽象类也不符合继承的"is-a"关系,这里不适用继承(还是上面那句话,行为根本不是个体)
于是,我们就迫切需要一种全新的数据类型,这种全新的数据类型:
- 不强调属性,只强调行为的抽象,"继承"它表示功能的扩展。
- 在一个类继承某个类后,还能继续"继承",不受Java单继承限制。
- 没有"is-a"这种继承关系的限制。
- 最好还有抽象方法。(因为它能表示规范和标准)
这种全新的数据类型,就是接口(interface)。
如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们实现这个接口,各自实现自己具体的处理方法来处理那个目标
定义
上面四条,其实已经描述了接口的特点,接口是一种表示对行为抽象的,不受Java多继承限制的,用来作为功能扩展标准的一种引用数据类型。它的定义语法如下:
[访问权限修饰符] interface 接口名{
}
注:
接口的访问权限修饰符也只有两种:
- public
- 默认缺省的
接口名和类名一样,遵循大驼峰式的书写规范,接口名最好起的见名知意。有些程序员喜欢在接口前加一个大写字母I来直接,表示该类型是一个接口,可以借鉴,但不强制。
接口中可以定义抽象方法,和抽象类中定义抽象方法没有区别。
接口是可以被一个类"继承"的,接口的"继承",更准确、常见的叫法称之为接口的实现。当然它也不再使用extends关键字,而是使用 implements 关键字。一个普通类实现接口必须实现所有抽象方法。它的语法是:
[访问权限修饰符] class 类名 extends 类名 implements 接口名 {}
细节问题:如果一个类实现接口并继承别的类,要"先继承再实现"。关键字位置不要写反了。
在接口的实现中,可以称呼接口为"父接口",接口的实现类为"子类",它们仍然是父子关系。当然多态现象仍然能够发生。
接口不受多继承限制,一个类可以在继承别的类的同时实现接口,而且可以实现多个接口。
优点
使用接口有如下优点:
- 接口不受单继承限制,这是接口非常重要的优点。
- 接口不是类,它和它的实现类,也不需要存在"is-a"关系,接口比抽象类的使用更加灵活。
- 程序开发中将接口和它的实现类之间的关系,称之为"like-a"关系。只要是实现了Xxx的功能,就像Xxx。
- 接口表示一种开发标准,一种规范。表示对功能的扩展,它只关注行为,不关注属性。
特点和使用
接口的特点我们主要研究以下方面
- 接口的声明特点
- 接口能不能创建对象?
- 接口能不能用final修饰?
- 接口的实现类的书写格式
- 成员特点
- 能否定义属性(成员变量, 静态成员变量, 全局常量)?
- 能否定义方法/行为?
- 构造器
- 代码块
测试后,我们的结论是:
接口的声明特点
- 接口能不能创建对象?
- 不能创建对象,实际上接口是抽象的. 因为接口的声明修饰符中隐含了关键字abstract
- 接口能不能用final修饰?
- 不能,接口定义出来目的就是为了被实现. final天生和接口矛盾
- 接口能不能创建对象?
接口的命名特点(非强制)
- 接口在命名时,为了体现接口的特殊性,表示它是一个接口. 某些程序员会在接口的命名前加大写字母"I"
接口的实现类的书写格式(非强制)
- 某些程序员会在接口的所在位置,定义一个全新的包,叫"impl"
- 然后在该包下写接口的实现类,命名为"接口名 + Impl"
成员特点
- 能不能定义属性?(成员变量,静态成员变量,全局常量)
- 可以定义,但是接口中的属性默认都是"public static final"修饰的公共的全局常量
- 这些隐含的修饰符不要写出来, 接口能够定义的所有结构的访问权限都是public, 而且public是隐含的修改不了
- 接口中没有静态代码块,接口中的全局常量,必须显式赋值
- 接口表示对功能的扩展,所以接口往往只有对行为的抽象(接口往往只有方法),接口是非常少见定义属性的, 只有当接口的所有实现类,都需要一个共同的全局常量时才考虑在接口中定义属性.
- 能不能定义方法/行为?
- 可以定义方法,但是接口中的方法默认都是"public abstract"修饰的公共的抽象方法
- 实际上,在Java8之前,接口中没有任何实现方法,接口中只能定义抽象方法
- 特别强调: 实际开发中,99%的情况下,接口就只有光秃秃的抽象方法定义,没有其他任何内容
- 能不能定义属性?(成员变量,静态成员变量,全局常量)
构造器
- 接口是没有构造器的,接口自己用不到构造器
接口的子类也用不到
代码块
- 没有静态代码块
没有构造器就没有构造代码块
以上总结,接口能定义的结构特点:
- 可以定义全局常量
可以定义抽象方法(Java8以后允许实现方法)
- 实际上,接口往往只有抽象方法定义,它表示对行为的抽象, 表示制定标准和规范
注意两个问题:
- 实现接口的类,如果没有明确继承一个其他类,那么子类对象的隐式初始化super()指向Object类。
- 接口虽然不是类,但是它也继承了Object类。
接口的使用,我们主要探讨它的子类特点。
接口的子类可以是什么?
普通类实现接口,必须实现所有的抽象方法。
抽象类实现接口,按需实现抽象方法。
接口可以继承一个接口,并且接口在继承接口后,可以重写父接口中的抽象方法和默认方法。
参考如下代码(了解即可):
interface A { Object test(); default void test2() { } } interface B extends A { /* 重写父接口抽象方法 只有在父接口的方法返回值类型是父类类型时 子接口中可以将抽象方法返回值类型重写为子类类型 其余地方都不可变。 */ @Override Demo test(); @Override default void test2() { System.out.println(); } }
extends VS implements
实现(Implement):如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标
继承: 指的是一个类(称为子类)继承另外的一个类(称为父类)的功能,并可以增加它自己的新功能的能力。所以,继承的根本原因是因为要复用,而实现的根本原因是需要定义一个标准
简单点说,就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。
class Car extends Benz implements GasolineCar, ElectroCar{
}
我们定义了一辆汽车,他实现了电动车和汽油车两个标准,但是他属于奔驰这个品牌。像上面这样定义,我们可以最大程度的遵守标准,并且复用奔驰车所有已有的一些功能组件。
继承(extends)是不能跨越种族的,类和类继承,接口和接口继承,接口和类之间没有继承关系。
- 普通类继承一个抽象类,需要实现里面的所有抽象方法。
- 抽象类继承一个抽象类,按需实现里面的抽象方法。
- 抽象类继承一个普通类,是可以的。
- 接口在继承一个接口后,会得到接口的所有抽象方法。
- 类的继承是单继承的,接口的继承是多继承的。
实现(implements),必须发生在类与接口之间,接口和类是没有任何关系的。
- 普通类实现接口,必须实现所有抽象方法
- 抽象类实现接口,按需实现抽象方法
默认方法和静态方法
Java8中接口的语法迎来了大的变动,新增了两种具有方法体的实现方法:
- 默认方法
- 静态方法
默认方法。
// 默认方法的语法: default 返回值类型 方法名(形参列表){ //方法体 }
- 接口中的默认方法的访问权限修饰符是public,不可更改。
- 默认方法的作用:
- 抽象类中的实现方法是用来作为子类的默认实现的,但是接口不具备这个功能。接口表示对功能的扩展,它更多的是一种标准和规范,多数情况下它不需要实现方法。
- Java8仍然新增了默认方法,主要是两个角度考虑:
- 接口中一旦有了默认实现方法,程序设计接口的灵活性就增加了。在没有默认方法时,接口中一旦新增任何方法,子类都必须实现它。而现在有了默认方法,就不需要这么做:需要实现的就自己重写,不需要的就直接继承。代码的兼容性和灵活性就增加了。
- Java8中引入了Lambda表达式和StreamAPI,接口中的默认方法可以作为它们的实现,完成函数式编程。(这个仅了解,后面会讲Lambda表达式)
- 接口中的默认方法,还可以配合接口的"多实现",实现真正意义上的多继承。
静态方法。
static 返回值类型 方法名(形参列表){ //方法体 }
- 接口中的静态方法也是默认public修饰的,不可更改。
- 它的调用方式是用接口名.静态方法名。
- 接口中的静态方法也是为了配合Lambda表达式和StreamAPI使用的,其余场景不多见。
- 接口自从Java8以后有了静态方法,甚至可以写一个main方法。
- 接口中的静态实现方法,只能自己用,不能用实现类类名调用。
上面两种方法仅供了解,实际开发中,可能会偶尔见到它,但自己写的概率非常小。
练习
- 教练(Coach)和运动员(Sportsman)案例
小试牛刀请用所学知识分析:这个案例中有哪些抽象类,哪些接口,哪些具体类。
现在有乒乓球运动员和篮球运动员,乒乓球教练和篮球教练。为了能够出国交流,跟乒乓球相关的人员都需要学习英语。
- (重要)某电商系统后台,前端程序员需要后端程序员,提供接口来完成商品的管理,展示页面。假如前后端商量好,要求能实现CRUD操作, 采用MVC分层思想, 业务逻辑如下:
- 展示全部商品(Products)页面时,将仅装有商品对象的数组返回,前端不提供任何数据。
- 新增商品时,前端提供商品各种参数,后端需要校验ID唯一性,然后将对象插入数组,返回插入结果(true/false)。
- 查询商品, 前端提供商品ID或者商品名称(可根据ID查询或者根据商品名称查询), 后端根据信息查询商品详情, 并返回结果(商品对象)
- 更新商品(每次更新商品中的一个信息即可), 前端提供要被更新的商品ID, 以及新的产品信息(可以用数组的形式存储新的产品信息), 后端根据商品ID找到该商品, 并将新的信息替换, 返回该商品对象.
- 删除商品时,前端提供商品ID,后端将该商品删除后,返回删除结果。(成功返回true,根据ID找不到该商品即删除失败返回false)
方法的形参和返回值类型
方法传值
方法的形参在方法调用传入实参时,不一定要求数据类型完全一致。
- **基本类型:**对于基本数据类型的方法形参,存在自动类型提升。
- 引用类型: 对于引用数据类型的方法传参,存在自动向上转型。
- 形参如果写一个普通类:调用方法时需要传入的是该类的对象或者该类的子类对象
- 形参如果写一个抽象类:调用方法时需要传入的是该抽象类的子类对象
- 形参如果写一个接口:调用方法时需要传入的是该接口的子类对象
方法的返回值
方法的返回值类型,和在方法体中返回具体值(对象)时,不一定要求数据类型完全一致。
- **基本类型:**方法体中,返回一个具体的值不要求和返回值类型完全一致,存在自动类型提升。
- 引用类型: 在方法体中,返回一个对象时不要求就是返回值类型的对象,存在自动向上转型。
- 返回值类型如果写一个普通类:可以返回该类的对象或者该类的子类对象
- 返回值类型如果写一个抽象类:返回该抽象类的子类对象。
- 返回值类型如果写一个接口:返回该接口的子类对象。
方法重写中的返回值类型问题
父子类方法重写中,方法声明中返回值类型的书写。
- **基本类型和void:**必须保持一模一样,不存在类型提升。
- **引用类型:**不必保持一模一样,存在自动向上转型。
- 类:父类中的方法返回一个普通类类型
- 子类方法中可以返回该类型
- 也可以返回该类的子类类型
- 抽象类:父类中的方法返回一个抽象类类型
- 子类方法可以返回该类型
- 也可以返回抽象类的实现类类型
- 接口:父类中的方法返回一个接口类型
- 子类方法可以返回该类型
- 也可以返回接口的实现类类型
- 类:父类中的方法返回一个普通类类型
链式调用
链式调用,在Java代码中是很常见的。当然它不是一个语法,这里讲一下它的形式,大家知道即可。
对于类Studnet:
class Student{
public Student getStudent(){
return new Student();
}
public Teacher getTeacher(){
return new Teacher();
}
}
class Teacher{
public void show(){
System.out.println("秀一波~~~");
}
}
假如在main方法中写以下代码(不使用链式调用):
StudentDemo sd = new StudentDemo();
Student s = sd.getStudent();
s.show();
使用链式调用的形式为:
new StudentDemo().getStudent().show();
链式调用的本质在于:前面方法的返回值是一个对象。
接口 VS 抽象类
接口和抽象类的异同:
编号 | 区别点 | 抽象类 | 接口 |
---|---|---|---|
1 | 定义 | 包含抽象方法的类 | 抽象方法和全局常量的集合 |
2 | 组成 | 构造方法、抽象方法、普通方法、常量、变量 | 常量、抽象方法、(jdk8:默认方法、静态方法) |
3 | 使用 | 子类继承抽象类(extends) | 子类实现接口(implements) |
4 | 关系 | 抽象类可以实现多个接口 | 接口不能继承抽象类,但允许继承多个接口 |
5 | 对象 | 不能创建对象,但是有构造方法 | 不能创建对象,也没有构造方法 |
6 | 局限 | 抽象类不能被多继承 | 接口之间能多继承,能被多实现 |
7 | 思想 | 作为模板或对共性属性和行为抽象,is-a | 作为标准或对共性行为抽象,like-a |
8 | 访问权限 | 抽象类的成员,写访问权限比较自由。 | 接口的成员,必须是public修饰的 |
9 | 选择 | 如果抽象类和接口都可以使用的话,优先使用接口,避免单继承的局限 |
总之,抽象类和接口除了都是抽象外,区别相当明显。抽象类是作为继承层次中的顶层父类存在的,接口则比较自由。
内部类
- 熟悉不同内部类的语法
- 熟悉不同内部类的访问特点
引例
引例
我们现在做一个应用程序,需要描述一台电脑中的CPU,对于电脑而言,该怎么去描述这个CPU呢?
分析:
我们可以创建两个类,一个描述Computer,一个描述CPU。但这样明显是有问题的:
- CPU是电脑中最重要的组件,但它脱离电脑存在就是一粒沙子。这意味着CPU对象,应仅存在于Computer内部。
- CPU可以控制计算机的一切硬件资源。这意味CPU对象能够操控,Computer类中的所有成员,无论私有。
定义普通类CPU是不能完成需求的,因为:
- 普通类没有私有概念,不可能控制仅能在Computer内部创建对象。
- 普通类不可能直接访问,其它类的私有成员。
显然CPU类不应该是一个普通类,不应该是一个独立的类 ---> 不再将CPU类定义为一个独立的类,而是将它放入Computer类的类体中,可以近似的看成是一个Computer类的成员 ---> CPU类近似看成Computer类的成员,于是:
- 在其它类内部的类,就有了私有概念,就有了四种访问权限。只需要在内部私有化一个类,那么在外界就无法创建它的对象了。
- 既然已经成为了成员,成为了"自己人",那么就不再受限于权限控制了,内部类就可以访问包裹它的类的所有成员,包括私有。
上述两条,其实就是(成员)内部类的主要特点。也说明了使用内部类的主要场景:
- 内部类是一种比类更加极致的封装思想体现,内部类的成员不仅被类自身保护,还被包裹着它的类保护。
- 内部类和包裹它的类互相成为了"好兄弟",互相访问成员都不受访问权限限制。
当你有以上使用场景时,就可以考虑使用内部类。
定义与分类
基本概念:
在Java语言中类可以嵌套定义,广义的内部类指的是定义在另一类当中的一个类。
分类:
根据内部类在类中定义的位置不同:
- 成员位置
- 成员内部类
- 静态内部类
- 局部位置
- 局部内部类
- 匿名内部类
- Lambda表达式
但是我们在学习的时候,不按定义位置来学习。我们按照它是一个类,还是一个对象来分类:
- 语法定义了一个类,包括成员内部类、静态内部类和局部内部类。既然是定义了一个类,使用时还需要创建对象才能用。
- 语法直接创建了一个对象,包括匿名内部类和Lambda表达式。由于已经通过语法创建了对象,可以直接使用。
// 外部类
class Computer{
// 内部类
class CPU{
}
}
// 外部其他类
class Demo{
}
为了上课方便,我们需要统一口径,在内部类课程当中,我们统一规定:
- 像CPU这种,定义在别的类的内部的类,我们称之为内部类.
- 像Computer这种,包裹内部类的,我们称之为外部类.
- Demo这种类我们称之为外部其他类.
学习方式: 1.定义的位置 2.权限修饰符 3.成员特点 4.继承和实现 5.访问特点 内部类----->外部类 外部类----->内部类 内部类----->外部其他类 外部其他类--->内部类
成员内部类
成员内部类是最普通的内部类,它定义在另一个类的成员位置, 可以看成该类的一个成员。
语法:
//外部类
[访问权限修饰符] class OuterClazz{
//成员内部类
[访问权限修饰符] class InnerClazz{
}
}
自身特点
访问权限修饰符
成员内部类可以看成另一个类的成员,它和类中成员一样,有四种访问权限级别:
- public
- protected
- 缺省的,默认访问权限
- private
注:
- 外部类可以保护成员内部类,如果想要访问一个成员内部类。首先需要外部类权限,然后还需要成员内部类权限。
- 都定义成员内部类了,多数情况下,应该私有化它。
成员特点
- 可以定义普通成员变量,成员方法。但没有静态声明(包括静态变量,静态方法,静态代码块)
- 有构造器,有构造代码块。
- 可以有静态全局常量。
注意:
- 允许定义字面值常量赋值的全局常量, 也就是不会触发类初始化的全局常量
继承和实现
- 内部类可以继承和实现外部的类和接口。
- 也可以在类中定义多个普通类、抽象内部类和接口用来自己继承和实现。
成员内部类的访问特点
在具体了解成员内部类的使用之前,我们要搞清楚成员内部类和外部类之间,到底什么关系?
成员内部类可以看成外部类的一个成员
类中的成员变量,必须依赖于类的对象而存在, 那么成员内部类的对象,也必须依赖于外部类对象而存在
也就是说,要想得到成员内部类对象,必须在外部类对象的基础上创建,成员内部类对象不能单独创建, 成员内部类依赖于外部类,反过来,没有依赖关系
之所以存在这样的依赖关系:
是因为成员内部类对象的成员变量列表中,都会持有外部类对象的引用(成员内部类对象中保存了一个它所寄生的外部类对象的引用)
成员内部类内部访问外部类(重点)
在成员内部类的成员方法中访问外部类成员,因为外部类对象已经存在,所以可以直接访问,直接写成员的名字就可以了。在特殊情况下,当成员内部类成员和外部类成员同名时,如果仍然用成员名直接访问,访问的结果是成员内部类自身成员(就近原则),这时:
- 在成员内部类的成员方法中,自身对象用this指向,用this点访问到的一定是自身成员。
- 在成员内部类的成员方法中,外部类对象用外部类类名.this指向,用外部类类名.this点访问到的一定是外部类成员。
极限情况下,出现局部变量,成员内部类成员变量,外部类成员变量三者都同名的情况:
- 就近原则直接写成员名字,访问的是局部变量的。
- 用this.访问的成员内部类自己的成员变量。
- 用外部类类名.this.访问的是外部类的成员变量。
最后,如果全局常量同名了,就用类名去区分好了。
外部类访问成员内部类成员(重点)
第二,外部类访问成员内部类成员,要分两种情况:
外部类的成员方法中访问成员内部类成员:
- 因为这时成员内部类对象不存在,就需要先创建它的对象。
- 成员内部类对象依赖外部类对象而存在, 所以得先存在外部类对象才行。而恰好成员方法中隐含自身对象,所以这里创建成员内部类对象的方式就是—— 直接new创建。
创建对象后,直接用对象名点访问即可。如果有同名的情况,用成员内部类对象名访问的就是内部类的成员,反之不写对象名或者写this访问的都是外部类的成员。
外部类的静态成员方法中访问成员内部类成员:
成员内部类对象不存在,就需要先创建它的对象。
成员内部类对象依赖外部类对象而存在, 所以得先存在外部类对象才行。但静态方法中什么对象都没有,就需要先创建外部类对象,再创建成员内部类对象。
语法:
OuterClazz outer = new OuterClazz(); InnerClazz inner = outer.new InnerClazz(); // 上面等价于下面 InnerClazz inner = new OuterClazz().new InnerClazz();
得到两个对象后,用谁的对象名访问的就是谁的成员。
外部其他类访问成员内部类成员(了解)
外部其他类要访问成员内部类成员,条件要苛刻的多。由于成员内部类属于外部类的一个成员,所以首先外部其他类需要有外部类的访问权限,再次还需要成员内部类的访问权限。
OuterClazz.InnerClazz oi = new OuterClazz().new InnerClazz();
创建对象后,用对象名访问成员即可。
成员内部类访问外部其他类成员(了解)
在成员内部类中访问外部类成员,和在普通类中访问其它类成员别无二致:
- 静态成员直接类名点访问。
- 普通成员需创建外部类对象去访问。
练习
试着说一说下述访问,能否进行,怎么进行,是否受访问权限限制。
- 成员内部类的成员方法中,去访问外部类的成员。
- 外部类的成员方法中,去访问成员内部类的成员。
- 外部类的静态成员方法中,去访问成员内部类的成员。
- 外部其他类的成员方法中,访问成员内部类的成员。
- 外部其他类的静态成员方法中,访问成员内部类的成员。
补全程序,使得可以输出三个num
class Outer { public int num = 10; class Inner { public int num = 20; public void show() { int num = 30; System.out.println(); System.out.println(); System.out.println(); } } }
静态内部类
有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部, 并不需要内部类有外部类对象的一个引用, 如果使用static来修饰一个成员内部类, 就不会生成那个引用, 则这个内部类就属于外部类本身, 而不是属于外部类的某个对象, 这就是所谓的静态内部类.
//外部类
[访问权限修饰符] class OuterClazz{
// 静态内部类访问权限修饰符,有四个,和普通成员一样
[访问权限修饰符] static class StaticInnerClazz{
}
}
自身特点
依然主要从以下几个角度分析:
- 访问权限修饰符
- 成员特点
- 继承和实现
访问权限修饰符
静态内部类和成员内部类一样,有四种访问权限级别:
- public
- protected
- 缺省的,默认访问权限
- private
注:
- 外部类可以保护静态内部类,如果想要访问一个静态内部类。首先需要外部类权限,然后还需要静态内部类权限。
- 既然定义内部类,仍然建议私有化。当然具体情况具体分析
成员特点
要理解静态内部类的成员有什么特点,就要像成员内部类那样,搞清楚,静态内部类到底和外部类是什么关系。在Oracle公司官网有一段文字解释静态内部类和成员内部类的区别:
Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.
这句话直白的翻译过来就是:
声明为static的嵌套类称为静态嵌套类,非static嵌套类才被成为内部类。
理解这句话,关键点就在于nested和inner的区别:
- nested,嵌套,指的是:直接把一个类丢到另一个类中,两个类其实没太大关系。
- inner,内部,指的是:某个类本身就是另一个类的一部分,在内部。
这其实就已经说明白了,成员内部类和静态内部类的区别:
- 成员内部类必须依赖外部类存在,创建成员内部类对象必须持有外部类对象的引用。
- 静态内部类和外部类就是独立的两个类,只不过静态内部类借用外部类来保护自己罢了。
相比较而言,成员内部类和外部类的关系是:心脏——身体,CPU——计算机
而静态内部类和外部类的关系是:寄居蟹——螺壳,啃老族——他的父母
继承和实现
静态内部类的继承与实现和成员内部类并没有太大不同,区别在于,静态内部类只能继承一个静态内部类,而不能继承成员内部类。而且由于静态内部类的独立性很强,在外部,也可以在有权限的情况下,轻松继承一个静态内部类。
总结
静态内部类很特殊, 因为静态内部类对象和外部类对象完全独立, 静态内部类对象不会持有外部类对象引用,所以它是内部类中的异类。实际开发中,你就将它作为一个可以定义在类的内部,隐藏自身存在的一个普通类,去使用就可以了。
静态内部类访问特点
在研究具体的访问前,仍先明确以下两点:
- 内部类和外部类是"好兄弟",它们之间的访问不受访问权限限制,包括私有。
- 静态内部类对象完全不依赖于部围类对象而存在,这意味着,互相访问,不管在哪都要创建对方对象。
静态内部类内部访问外部类
不管是静态内部类中的静态方法还是成员方法,都没有外部类对象存在,需要创建对象访问。语法就直接new对象就可以了。创建好对象以后,用对象名访问即可。如果同名,有谁的引用访问的就是谁的。
外部类访问静态内部类成员
不管是静态内部类中的静态方法还是成员方法,都没有外部类对象存在,需要创建对象访问。语法就直接new对象就可以了。创建好对象以后,用对象名访问即可。如果同名,有谁的引用访问的就是谁的。
外部其他类访问静态内部类成员
主要就是考虑权限,先要有外部类权限,再要有静态内部类权限。有权限后就可以创建对象了,但是创建对象时,需要指出外部类是谁(毕竟你住人家房子里),语法是:
OuterClazz.StaticInnerClazz inner = new OuterClazz.StaticInnerClazz();
创建对象后,用对象名访问成员即可。当然这个访问受权限限制。
静态内部类访问外部其他类成员
创建对象访问即可,受权限控制
注意事项
内部类也是类,也需要类加载, 静态内部类和外部类之间的类加载,它们会互相影响吗?
是不会的, 静态内部类和外补类之间本身没有依赖关系
它们的类加载,new对象,都是没有关系的
练习
试着说一说下述访问,能否进行,怎么进行,是否受访问权限限制。
- 静态内部类的成员方法中,去访问外部类的成员。
- 外部类的成员方法中,去访问静态内部类的成员。
- 外部类的静态成员方法中,去访问静态内部类的成员。
- 外部其他类的成员方法中,访问静态内部类的成员。
- 外部其他类的静态成员方法中,访问静态内部类的成员。
总结一下,成员内部类和静态内部类的用途:
首要用途是隐藏类,当你在某个位置需要一个对象来完成需求,而你又不希望外界知道这个类时,可以用内部类, 用的时候优先用静态内部类,因为它限制比较少
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,简单来说,将局部内部类看成是局部变量即可,该类的有效范围仅在作用域内部。(这意味着要创建它的对象,必须在作用域内部创建)
从语法和具体使用上看,局部内部类和成员内部类很相似,有点类似于直接把一个成员内部类放进局部位置,语法是:
// 局部位置
class Inner{
}
// 局部位置
自身特点
访问权限修饰符
局部内部类和局部变量一样,没有访问修饰权限。因为这毫无意义,大括号已经限制了它的访问范围。同样,局部内部类不能用static关键字修饰,原因和局部变量一样。
成员特点
局部内部类的成员特点和成员内部类一模一样,不再赘述:
- 没有静态static声明,但可以创建全局常量(不触发类加载的)
- 有构造方法和构造代码块。
继承和实现
局部内部类可以继承和实现外部的类或者接口,这是局部内部类的一个重要用途。
局部内部类的访问特点
- 外部类仍然把局部内部类当"好兄弟",局部内部类访问外部类成员仍不受权限限制。但局部内部类的作用域已被限制死了,外部类中只有装着局部内部类的作用域内,能访问到该局部内部类。
- 外部其他类已经完全无法访问到局部内部类了。
局部内部类在外部类的成员方法中
外部类的成员方法中,是隐含自身类对象的引用的,并且这个引用编译器会自动加入到局部内部类中。也就是说, 处在外部类成员方法中的局部内部类,会持有外部类对象的引用。
于是:
- 可以直接在局部内部类的成员方法中,访问外部类的成员。
- 如果出现同名,仍然用外部类类名.this指向外部类对象。
- 如果全局常量同名,就用类名点区分。
局部内部类在外部类的静态成员方法中
静态方法中就不存在任何对象了,其中的局部内部类也就不会持有引用了。想要访问外部类成员,就需要创建对象了。
局部内部类的经典使用
局部内部类经常使用在以下两个场景中:
方法需要返回一个对象,返回值类型是引用数据类型时。
方法需要返回一个对象时,可以在方法的局部位置写一个 局部内部类 继承/实现外部的类/接口,创建对象后作为返回值。这是因为方法的返回值可以是返回值类型的对象,也可以是子类对象(当返回值类型是引用数据类型时)。
方法需要传入一个对象,形参数据类型是引用数据类型时。
方法需要传入一个对象实参时,可以在调用方法的位置定义一个 局部内部类 来继承/实现外部的类/接口,创建对象后作为方法的实参传入。这是因为方法的实参可以是形参类型的对象,也可以子类对象(当返回值类型是引用数据类型时)。
使用局部内部类的优点:
- 绝对对外界隐藏,封装。
- 相比较于传统的定义类,然后创建对象,它相对更加简洁省事。
缺点:
- 这个类是一次性的。
综上,假如在局部位置需要一次性的使用某个对象,可以使用局部内部类创建它,但是如果多次在不同的方法中使用,使用局部内部类就得不偿失了。
注意事项
一. 在局部内部类的成员方法中,如果想要访问作用域内部的局部变量,那么该变量:
- 要么直接就用final修饰,是一个局部常量。
- 要么就是一个事实上的常量,即只能赋值一次,相当于用final修饰它。
在Java8之前版本的JDK中,如果不给能在局部内部类内部,访问的局部变量直接加上final修饰,会编译报错。
在最后,特别强调一点。后面学习的 匿名内部类 和 Lambda表达式 本质依然是局部内部类,这一条注意事项仍然生效。
二. 局部内部类对象作为方法的返回值, 返回值类型必须是其父类型
内部类的优缺点和使用场景
- 场景一:无条件地访问外部类的所有元素(优点)
- 场景二:隐藏类
- 可以用private、protected修饰内部类。
- private修饰内部类、外界感受不到该类存在。
- 场景三:实现多继承
- 可以创建多个成员内部类继承外部多个类
- 然后创建内部类对象,实际上就是外部类继承了多个类的成员
- 场景四:通过匿名内部类来优化简单的接口实现/Lambda表达式更简洁
- 重点:内部类要说使用频率 最高的肯定是匿名内部类和Lambda表达式
内部类的缺点
内部类的缺点也是显而易见,语法很复杂,在类中定义内部类也会导致类的结构变复杂,影响代码可读性。
除此之外,不合理使用内部类还可能导致内存泄漏(了解)
持有外部类对象引用的内部类对象,如果始终被使用而没有释放:
这在极端的场景下,会导致堆内存溢出,存在一定的风险。
内部类对象
学习目标
- 熟练掌握匿名内部类使用
- 熟练掌握lambda表达式使用
概述
本节内部类对象,本质上还是属于定义在别的类内部的一个类。
但区别是: 成员内部类、静态内部类和局部内部类都属于定义了一个类,后续使用还需要基于内部类创建对象。而本节中的匿名内部类和Lambda表达式都是直接通过语法创建了对象。
最后,在学习匿名内部类和Lambda表达式之前,我们还是要清楚——它们都是特殊的局部内部类。所以一旦访问方法的局部变量,该局部变量需要是一个常量(final 修饰)。
匿名内部类
我们已经使用过匿名对象了,匿名对象的含义是这个对象是没有名字,没有引用指向它。那么匿名内部类,指的就是这个内部类没有名字。当然成员内部类和静态内部类没法没有名字,所以 匿名内部类指的是"没有名字的局部内部类"。
语法
在方法等局部位置,写下列语法,即表示定义匿名内部类:
// 局部位置
new 类名或者接口名(){
// 某类名或接口名的子类的类体
};
// 局部位置
解释说明:
- new表示创建对象,表示创建语法中"类名/接口名"的子类对象。
- 这个语法结构,我们只知道这是一个子类对象,但这个子类到底叫啥,不知道,所以它是一个匿名类。当然匿名内部类更准确的说法,应该叫匿名内部类对象。
- 匿名内部类的本质是,一个继承了类或者实现了接口的匿名子类对象。
基本使用
匿名内部类是特殊的局部内部类,它的成员特征、访问特征和局部内部类没有区别。这里不再赘述。
这里我们直接说明一下,匿名内部类对象的两种使用方式:
- 可以直接在后面调用方法,访问它的成员(当一个匿名对象使用)
- 优点是:可以访问子类独有的成员。方便快捷,不需要用引用接收再去使用
- 缺点是:一次性,仅能用一次
- 可以用(父)引用接收对象,然后再用引用访问成员
- 优点是:可以用多次
- 缺点是:不能访问子类独有成员(被父类引用限制了访问范围,无法强转,因为子类已经匿名了)
综上,两种使用方式场景不同。如果有多次使用需求,就需要父引用接收;反之如果仅用一次,或者需要访问子类独有成员,就必须直接使用,不能用引用接收。
使用场景
匿名内部类实际上就是更简单的局部内部类(直接获取了对象),所以局部内部类的使用场景,可以直接套用过来。
匿名内部类经常使用在以下两个场景中:
方法需要返回一个对象,返回值类型是引用数据类型时。
方法需要返回一个对象时,可以在方法的局部位置写一个 基于X类或X接口的匿名内部类对象 ,然后直接作为返回值返回给外界。
方法需要传入一个对象,形参数据类型是引用数据类型时。
方法需要传入一个对象实参时,可以在调用方法的实参位置填入一个 基于X类或X接口的匿名内部类对象,就将它作为一个实参传给方法使用。
使用匿名内部类的优点:
- 绝对对外界隐藏,封装。
- 比起局部内部类,它更方便简洁了。所以实际开发中,匿名内部类基本取代了局部内部类的作用。
缺点:
- 这个对象是一次性的。
总之,酌情使用匿名内部类对象,可以简化代码书写,方便省事。但不要为了使用而使用,假如存在多个场景都需要一个子类对象,那还是直接创建一个子类出来好了。
练习
不修改main方法和接口Inter的前提下,补齐Outer类代码,要求在控制台输出HelloWorld
public class Demo{
public static void main(String[] args) {
Outer.method().show();
}
}
interface Inter {
void show();
}
class Outer {
}
几种实现方式:
- 手写类实现接口
- 局部内部类
- 匿名内部类
- lambda表达式
lambda表达式
Lambda 表达式是 JDK8 的一个新特性,可以取代接口的匿名内部类,写出更优雅的Java 代码。
- Lambda表达式仍然是局部内部类,是特殊的局部内部类,仍然定义在局部位置。而且局部内部类的注意事项,也一样存在。
- Lambda表达式在取代匿名内部类时,不是全部都取代,而是取代接口的匿名内部类,而类的匿名内部类Lambda表达式是不能取代的。
- Lambda表达式是匿名内部类的更进一步, Lambda表达式得到的也不是一个类,而是一个对象,并且是接口的子类对象。
使用前提
Lambda表达式虽然说是取代接口的匿名内部类,但也不是什么接口都能用Lambda表达式创建子类对象。
Lambda表达式要求的接口中,必须有且仅有一个必须要实现的抽象方法。这种接口在Java中,被称之为"功能接口"。功能接口在语法上,可以使用注解@FunctionalInterface标记在接口头上,用于检测一个接口是否是功能接口。
例如:
注解@FunctionalInterface
@FunctionalInterface
interface IA{
void test();
}
上述代码中IA
接口就是一个功能接口,注解就不会编译报错,反之就会编译报错。这有点类似于@Override注解对方法重写的检验。
看完上述关于功能接口的语法定义,思考两个问题:
- 功能接口中只能有一个方法吗?
- 功能接口中只能有一个抽象方法吗?
答:
不是,Java8中的默认方法和静态方法不需要子类实现,功能接口中可以允许有它们存在。
不是,有极个别比较特殊的抽象方法,可以不需要子类实现。
注:Object类是Java每一个类的父类,所以Object类当中的方法实现就可以作为接口抽象方法的实现。比如:
功能接口不仅有一个抽象方法
@FunctionalInterface interface IA{ void test(); boolean equals(Object obj); }
接口IA仍然是一个功能方法,因为抽象方法boolean equals(Object obj);可以直接使用Object类中的实现,无需子类实现。
所以,再强调一下,功能接口指的是,有且仅有一个必须要子类实现的抽象方法的接口。
基本使用
功能接口准备完毕后,就可以写Lambda表达式的语法,表示创建功能接口的子类对象了。当然Lambda表达式是特殊的局部内部类,仍然要写在局部位置。
它的语法是:
(形参列表) -> {
// 方法体
}
解释一下:
- (形参列表)表示功能接口中,必须要重写的抽象方法的形参列表。
- ->由一个英文横杠 + 英文大于号字符组成,它是Lambda表达式的运算符,读作goes to。
- { //方法体 }表示功能接口中,必须要重写的抽象方法的,方法体实现。
其实,看完这个Lambda表达式的语法定义,就已经能够解释为什么Lambda表达式,要求接口有且只有一个必须要实现的抽象方法了。因为语法中仅有一套形参列表和方法体,只能重写一个方法。
而且,这个语法写出来,很明显Lambda表达式只能重写父接口中的抽象方法,是不能自己新增成员的!
在方法等局部位置,写上述语法后,肯定会报错。原因在于Java是强类型语言,任何变量都有它的数据类型,而直接写Lambda表达式语言,编译器是无从得知它的数据类型的——这个Lambda表达式创建了接口的子类对象,到底是哪个接口的子类对象呢?
所以我们需要帮助编译器,明确Lambda表达式所表示的对象的类型,这个过程称之为 "Lambda表达式的类型推断"。
怎么推断呢?在这个推断过程中,需要给编译器提供额外的信息,告诉它Lambda表达式是哪个接口的子类对象。总得来说,常见和常用的有以下三种方式:
直接用父接口引用接收。由于Lambda表达式表示的子类对象并没有自己独有的成员,所以直接用父类引用接收完全不会有任何问题。
不用引用接收,但是要直接告诉编译器Lambda表达式是哪个接口的子类对象,语法上有点像强转(但不是)。
语法:
((父接口的名字)Lambda表达式).方法名(实参)
这种方式有点类似于匿名对象,所以必须直接调用方法,不然会编译语法报错。
借助方法完成类型推断。
- 可以借助方法的返回值数据类型完成类型推断,因为方法的返回值编译器已经知道该返回什么对象。
- 可以借助方法的形参的数据类型完成类型推断,因为方法的实参编译器已经知道该传入什么对象。
进阶使用和简化
为了方便语法练习,这里给出六个功能接口:
功能接口案例
//无返回值无参数的功能接口
@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表达式的进阶使用, 主要就是格式的简化
逐个部分简化:
(形参列表)能不能简化呢?是可以的,因为功能接口中有且仅有一个必须要实现的抽象方法,那么:
- 形参的数据类型是可以省略的,因为方法已经固定死了,形参一定是那些,不需要写出来。但形参的名字是不可省略的(因为要在方法体中使用)
- 特殊情况下:
- 如果形参列表中的形参只有一个,那么()小括号,也是可以省略的。
- 但是如果形参为空,()小括号是不可以省略的。
{ //方法体 }方法体能不能简化呢?当然是可以的:
- 如果方法重写的方法体只有一条语句的话,那么可以省略大括号。(类似于if/for省略大括号)
- 特殊的,如果只有一条语句且这条语句是返回值语句,那么大括号和return可以一起省略。
实际上在多数情况下,都不太可能一句话把方法体写完。多数情况下,Lambda表达式的抽象方法实现都会很复杂,那这样Lambda表达式就会写的很复杂,这就很难受了。而Lambda表达式,本质上就是重写了一个抽象方法的子类对象,所以Java允许Lambda表达式的抽象方法的实现可以直接指向一个已经存在的方法,而不是自己书写实现。这种语法在Java中称之为"方法引用"!
方法引用
Lambda表达式的主体只有1条语句时, 程序不仅可以省略主体的大括号, 还可以通过英文双冒号::的语法来引用方法, 进一步简化Lambda表达式的书写.
什么样的方法,能够作为方法引用指向的功能接口中抽象方法的实现?只看三点:
- 返回值类型必须一致。
- (方法签名method signature一致)形参列表中的数量,类型,位置必须都对应上,形参名字无所谓。
- 方法的名字无所谓。
Lambda表达式支持的方法引用
种类 Lambda表达式示例 对应的引用示例 解释说明 类名引用静态方法 (x, y, ...) -> 类名.静态方法名(x, y, ...) 类名::静态方法名 功能接口中被实现方法的全部参数传给该类静态方法作为参数 对象名引用成员方法 (x, y, ...) -> 对象名.成员方法名(x, y, ...) 对象名::成员方法名 功能接口中被实现方法的全部参数传给该成员方法作为参数 类名引用成员方法(特殊) (x, y, ...) -> **对象名x.**成员方法名(y, ...) 类名::成员方法名(不是静态方法) 功能接口中被实现方法的第一个参数作为调用者, 后面的参数传给该成员方法作为参数 构造方法引用 (x, y, ...) -> new 类名(x, y, ...) 类名::new 功能接口中被实现方法的全部参数传给该构造器作为参数
/*
Lambda表达式的主体只有1条语句时, 程序不仅可以省略主体的大括号,
还可以通过英文双冒号::的语法来引用方法, 进一步简化Lambda表达式的书写.
什么样的方法,能够作为方法引用指向的功能接口中抽象方法的实现?
只看三点:
1. 返回值类型必须一致。
2. (方法签名method signature一致)形参列表中的数量,类型,位置必须都对应上,
形参名字无所谓。
3. 方法的名字无所谓。
*/
/*
1.静态方法引用
2.对象名引用成员方法
3.类名引用成员方法
4.构造方法引用
*/
public class Demo {
public static void main(String[] args) {
// 静态方法引用
// lambda写法
IA ia2 = () -> System.out.println(111);
ia2.testA();
IA ia = () -> A.func1();
ia.testA();
// 方法引用
IA ia1 = A::func1;
ia1.testA();
// 对象名引用成员方法
// lambda
IC ic2 = (int a) -> {
System.out.println(a);
};
ic2.testC(1);
IC ic = a -> new C().func2(a);
ic.testC(100);
// 方法引用
IC ic1 = new C()::func2;
ic1.testC(200);
//IB ib = s -> System.out.println(s);
//ib.testB("aaa");
// 可以指向源码中的方法或者第三方工具的方法
IB ib = System.out::println;
ib.testB("aaa");
// "abcdef", 希望对字符串进行截取"bc"
// 主要使用String类中的成员方法
// subString( int start,int end) [start,end)
//String s = "abcdef";
//String substring = s.substring(1, 3);
//System.out.println("substring = " + substring);
// 类名引用成员方法
// lambda
ID id = (s, start, end) -> s.substring(start, end);
String str = id.testD("abcdef", 1, 3);
System.out.println("str = " + str);
ID id2 = String::substring;
String str2 = id2.testD("abcdef", 1, 3);
System.out.println("str2 = " + str2);
// 4.构造方法引用
// lambda表达式
// 有参构造
IE ie = (String name, int age) -> new Dog(name, age);
Dog dog = ie.getDog("小黑", 3);
System.out.println(dog);
// 无参构造
IG ig = () -> new Dog();
Dog dog1 = ig.getDog();
System.out.println(dog1);
// 方法引用
IE ie2 = Dog::new;
Dog dog2 = ie2.getDog("小花", 4);
System.out.println(dog2);
IG ig2 = Dog::new;
Dog dog3 = ig2.getDog();
System.out.println(dog3);
}
}
@FunctionalInterface
interface IA{
void testA();
}
class A {
// 定义一个静态方法 作为IA接口中的testA方法的实现
static void func1(){
System.out.println("IA接口中的testA方法的实现");
}
}
@FunctionalInterface
interface IB{
void testB(String s);
}
@FunctionalInterface
interface IC{
void testC(int a);
}
class C {
void func2(int m){
System.out.println(m);
}
}
@FunctionalInterface
interface ID{
String testD(String s, int start, int end);
}
@FunctionalInterface
interface IE{
Dog getDog(String a, int b);
}
@FunctionalInterface
interface IG{
Dog getDog();
}
class Dog{
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public Dog() {
}
}
注意事项
- 方法引用所指向的实现方法, 只要是已经存在的就可以, 可以自己写, 可以用源码的方法或者第三方工具类的方法
- Lambda表达式是特殊的局部内部类,所以它访问方法的局部变量,该变量必须是final的(实际常量)。
- 除此之外,局部内部类,匿名内部类它们都有自己独立的作用域,能够自定义自己的成员。 但是Lambda表达式没有自身独立的作用域,不能自定义成员,和所在的方法共用同一个作用域。
优缺点
优点:
极大得简化了代码,使代码变得更加优雅。
函数式编程的代表,可能是未来高端的编程趋势
- Lambda表达式在Stream API中,配合集合类去使用,代码非常优雅和简洁,并且高效,实际开发中十分常用。
Stream API代码
list. stream(). filter(stu -> stu.getAge() >= 18). map(Student::getScore). forEach(System.out::println);
注:该Stream API完成,将学生对象集合中的,所有大于等于18岁的学生的成绩输出的工作。
缺点:
- 过于简单的Lambda表达式,显然可读性很低。
- 过于简洁也意味着不容易Debug。
- 语法难度不低,熟练使用需要时间锻炼。