Java继承和多态
Q7nl1s admin

Java类的封装

封装将类的某些信息应藏在类内部,不允许外部程序直接访问,只能提供该类提供的方法来实现对隐藏信息的操作和访问。

封装的特点:

  • 只能通过规定的方法访问数据。
  • 隐藏类的实例细节,方便修改和实现,而不会对外部数据造成影响。

实现封装的具体步骤:

  1. 修改属性的可见性来限制对属性的访问,一般设置为private。
  2. 为每个属性系创建一对赋值(setter)方法和取值(getter)方法,一般设为public,用于属性的读写。
  3. 在赋值和取值方法中,加入属性控制语句(对属性值的合法性进行判断)。

Java继承简明教程

继承也是面向对象的三大特征之一。用于保留父类的一些特征,从而减少代码的冗余,避免反复造轮子。

Java中的继承就是在已存在类的基础上进行扩展。已存在的类被称之为父类、基类或超类,而新产生的类称为子类或派生类。子类不仅包含父类的属性和方法,还可以增加新的属性和方法。

继承父类的语法格式如下:

1
2
3
修饰符 class class_name extends extend_class{
// 类的主体
}

extends使Java中的关键字,专门在类的继承中使用,extends的英文意思是扩展,而不是继承。它很好的体现了子类和父类的关系,即————子类是父类的扩展,它是一种特殊的父类。

Java与C++定义继承方式非常类似。Java用关键字extends代替了C++中的冒号(:)。在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承。

由于Java中所有类的继承都是公有继承,因此类的继承不改变类成员的访问权限,即————如果父类的常用是公有的、被保护的或是默认的,它的子类具有相应的这些特征,并且子类不能获得父类的构造方法。

下面给出一个案例:学生和教室都属于人,它们拥有相同的属性:下面、年龄、性别和身份证号,而学生还具有学号和所学专业两个属性,教师还具有教龄和所教专业两个属性。

1)创建人类People,并定义name、age、sex、sn属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public calss People{
public String name; // 姓名
public int age; // 年龄
public String sex; // 性别
public String sn; //身份证号码

public People(String name,int age,String sex,String sn){
this.name = name;
this.age = age;
this.sex = sex;
this.sn = sn;
}

public String toString{
return "姓名:" + name + "\n年龄:" + age + "\n性别:" + sex + "\n身份证号码:" + sn;
}
}

如上代码,在People类中包含4个共有属性、一个构造方法和一个同String()方法。

2)创建People类的子类Student类,并定义stuNo个department属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student extends People{
private String stuNo; // 学号
private String department; // 专业

public Student(String name,int age,String sex,String sn,String stuNo,String department){
super(name,age,sex,sn);
this.stuNo = stuNo;
this.department = department;
}

public String toString(){
return "姓名:" + name + "\n年龄:" + age + "\n性别:" + sex + "\n身份证号码:"
+ sn + "\n学号:" + stuNo + "\n专业:" + department;
}
}

由于Student类继承自People类,因此,在Student类中同样具有People类的属性和方法,这里重写了父类中的toString()方法。

注意:如果在父类中存在有参的构造方法而没有重载无参的构造方法,那么子啊子类中必须含有有参的构造方法,因为如果在子类中不含有构造方法,默认会调用父类中午餐的构造方法,而在父类中并没有午餐的构造方法,因此会出错。

3)创建People类的另一个子类Teacher,并定义tYear和tDept属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Teacher extends People{
private int tYear; // 教龄
private String tDept; // 所教专业

public Teacher(String name,int age,String sex,Stirng sn,int tYear,String tDept){
super(name,age,sex,sn);
this.tYear = tYear;
this.tDept = tDept;
}

public String toString() {
return "姓名:" + name + "\n年龄:" + age + "\n性别:" + sex + "\n身份证号:" + sn + "\n教龄:" + tYear + "\n所教专业:" + tDept;
}
}

Teacher类与Student类相似,同样重写了父类中的toString()方法。

4)编写测试类PeopleTest,在该类中创建People类的不同对象,分别调用它们的toString()方法,输出不同的信息。具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PeopleTest{
public static void main(String[] args){
// 创建Student类对象
People stuPeople = new Student("王丽丽",23,"女","410521198902145589","00001","计算机科学与技术");
System.out.println("---------------------------------学生信息---------------------------------");
System.out.println(stuPeople);

// 创建Teacher类对象
People teaPeople = new Teacher("张文", 30, "男", "410521198203128847", 5, "计算机应用与技术");
System.out.println("---------------------------------教师信息---------------------------------");
System.out.println(teaPeople);
}
}

运行程序,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---------------------------------学生信息---------------------------------
姓名:王丽丽
年龄:23
性别:女
身份证号:410521198902145589
学号:00001
所学专业:计算机应用与技术
---------------------------------教师信息---------------------------------
姓名:张文
年龄:30
性别:男
身份证号:410521198203128847
教龄:5
所教专业:计算机应用与技术

单继承

Java摒弃了C++中多继承的特征,不再支持多继承,只允许一个类直接继承另一个类,即子类只能拥有一个直接父类,extends关键词后面只能跟有一个类名。如下代码会发生编译错误:

1
2
class Student extends Person,Person1,Person2{…}
class Student extends Person,extends Person1,extends Person2{…}

如果定义一个Java类时未显式指定这个类的直接父类,则这个类默认继承java.lang.Object类。因此,java.lang.Object类是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有Java对象都可以调用java.lang.Object类所定义的实例方法。

使用继承的注意点:

  • 子类一般比父类包含更多的属性和方法。
  • 父类中的private成员子啊子类中比可见,因此在子类中不能直接使用它们。
  • 父类和其子类间必须存在“是一个”即“is-a”的关系,否则不能用继承。但也不是所有符合“is-a”关系的都应该用继承。例如,正方形是一个矩形,但不能让正方形类来继承矩形类,因为正方形不能从矩形扩展得到任何东西。正确的继承关系是正方形继承图形类。
  • Java只允许单一继承(即一个子类只能有一个直接父类),C++可以多重继承(即一个子类拥有多个直接父类)。

Java super关键字

由于子类不能调用父类的构造方法,所有,如果要调用父类的构造方法,可以使用super关键字。super可以用来访问父类的构造方法、普通方法和属性。

super关键字的功能:

  • 在子类的构造方法中显式的调用父类的构造方法
  • 访问父类的成员方法和变量

super调用父类的构造方法

基本格式如下:

1
super(parameter-list);

parameter-list:父类构造方法的所有参数。super()必须是在子类构造方法的方法体的第一行。

下面给出一个案例:

声明父类Person,类中定义两个构造方法

1
2
3
4
5
public class Person{
public Person(String name,int age){}

publci Person(String name,int age,String sex){}
}

子类Student继承了Person类,使用super语句来定义Student类的构造方法:

1
2
3
4
5
6
7
8
9
public class Student extends Person{
public Student(String name,int age,String birth){
super(name,age); // 调用父类中含有2个参数的构造方法
}

public Student(String nane,int age,String sex,String birth){
super(name,age,sex); // 调用父类中含有3个参数的构造方法
}
}

编译器会自动在子类构造方法的第一句加上super();来调用父类的无参构造方法,必须写在子类构造方法的第一句,也可以省略不写。通过super来调用父类其它构造方法时,只需要把相应的参数传过去。

super访问父类成员

当子类成员变量或方法和父类同名时,可以使用super关键字来访问。如果子类重写了父类里的一个方法,即子类和父类有相同的方法定义,但是又不同的方法体,此时,我们可以通过super来调用父类里的这个方法。

使用super访问父类中的常用于this关键字的使用类似,只不过它引用的是子类的父类,语法格式如下:

1
super.member

member:父类中的一个属性或方法

super调用成员属性

当父类和子类具有相同的数据成员时,JVM 可能会模糊不清。我们可以使用super.member来访问父类成员属性,用member来直接访问子类的成员属性。

super调用成员方法

当父类和子类具有相同的方法名时和数据成员的调用类似,一者用super.member,一者直接用member

super和this的区别

this是当前对象的引用,super是当前父对象的引用。

super关键字的用法:

  • super.父类属性名:调用父类中的属性
  • super.父类方法名:调用父类中的方法
  • super():调用父类中的无参构造方法
  • super(参数):调用父类中的有参构造方法

如果构造方法的第一行代码不是this()和super(),则系统会默认添加super()。(和java.lang.Object类的存在有关)

this关键字的用法:

  • this.属性名:表示当前对象的属性
  • this.方法名(参数):表示调用当前对象的方法

关于 Java super 和 this 关键字的异同,可简单总结为以下几条。

  1. 子类和父类中变量或方法名称相同时,用 super 关键字来访问。可以理解为 super 是指向自己父类对象的一个指针。在子类中调用父类的构造方法。
  2. this 是自身的一个对象,代表对象本身,可以理解为 this 是指向对象本身的一个指针。在同一个类中调用其它方法。
  3. this 和 super 不能同时出现在一个构造方法里面,因为 this 必然会调用其它的构造方法,其它的构造方法中肯定会有 super 语句的存在,所以在同一个构造方法里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  4. this( ) 和 super( ) 都指的是对象,所以,均不可以在 static 环境中使用,包括 static 变量、static 方法和 static 语句块。
  5. 从本质上讲,this 是一个指向对象本身的指针, 然而 super 是一个 Java 关键字

其中着重要注意的是第3、4两点。

下面给出一个案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类Animal的定义
public class Animal{
public String name; // 动物名字
}

// 子类Cat的定义
public class Cat extends Animal{
private String name; // 名字

public Cat(String aname,String dname){
super.name = aname; // 通过super关键字来访问父类中的name属性
this.name = dname; // 通过this关键字来访问本类中的name属性
}

public String toString(){
return "我是" + super.name + ",我的名字叫" + this.name;
}

public static void main(String[] args){
Animal cat = new Cat("动物","喵星人");
System.out.println(cat);
}
}

Java对象的类型转换

本节说的对象的类型转换,是指存在继承关系的对象,不是任意类型的对象。党对不存在继承关系的对象进行强制类型转换时,会抛出Java强制类型转换(java.lang.ClassCastException)异常。

Java语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。Java中引用类型之间的类型转换(前提是两个类是父子关系)主要又两种,分别是向上转型(upcasting)和向下转型(downcasting)。

向上转型

父类引用指向子类对象为向上转型,语法如下:

1
fatherClass obj = new sonClass();
  • fatherClass:父类名称或接口名称
  • obj:创建的对象名
  • sonClass:子类名称

向上转型就是把子类对象直接赋给父类引用,不用强制转换。使用向上转型可以调用父类类型中的所有成员,不能调用子类类型中的特有成员。

向下转型

与向上转型相反,子类对象指向父类引用为向下转型,语法如下:

1
sonClass obj = (sonClass) fatherClass
  • fatherClass:父类名称
  • obj:创建的对象名称
  • sonClass:子类名称

向下转型可以带哦有子类类型中的所有成员,但是要注意的是,如果父类引用对象指向的是子类对象,那么子啊向下转型的过程中是安全的,也就是编译不会出现错误。但如果父类引用对象本身是父类,那么在向下转型的过程中是不安全的,编译也不会出错,但允许时会出现我们开始提到的Java强制类型转换异常,所有一般用instanceof运算符来避免出现此类错误。

1
2
Animal anmal = new Dog(); // 向上转型,把Dog类型转换为Animal类型
Dog dog = (Dog) animal; // 向下转型,把Animal类型转换为Dog类型

下面给出案例

父类Animal的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Animal {
public String name = "Animal:动物";
public static String staticName = "Animal:可爱的动物";

public void eat() {
System.out.println("Animal:吃饭");
}

public static void staticEat() {
System.out.println("Animal:动物在吃饭");
}
}

子类Cat的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Cat extends Animal {
public String name = "Cat:猫";
public String str = "Cat:可爱的小猫";
public static String staticName = "Dog:我是喵星人";

public void eat() {
System.out.println("Cat:吃饭");
}

public static void staticEat() {
System.out.println("Cat:猫在吃饭");
}

public void eatMethod() {
System.out.println("Cat:猫喜欢吃鱼");
}

public static void main(String[] args) {
Animal animal = new Cat();
Cat cat = (Cat) animal; // 向下转型
System.out.println(animal.name); // 输出Animal类的name变量
System.out.println(animal.staticName); // 输出Animal类的staticName变量
animal.eat(); // 输出Cat类的eat()方法
animal.staticEat(); // 输出Animal类的staticEat()方法
System.out.println(cat.str); // 调用Cat类的str变量
cat.eatMethod(); // 调用Cat类的eatMethod()方法
}
}

通过引用类型变量来访问所引用对象的属性和方法时,Java虚拟机将采用以下绑定规则:

  • 实例方法与引用变量时间引用的对象的方法进行绑定,这种绑定属于动态绑定,因为时在运行时又Java虚拟机动态决定的。例如,animal.eat()是将eat()方法与Cat类绑定。
  • 静态方法与引用变量所声明的类型的方法绑定,这种绑定属于静态绑定,因为是在编译阶段已经做了绑定。例如animal.staticEat()是将staticEat()方法与Animal类进行绑定。
  • 成员变量(包括静态变量和实例变量)与引用变量所声明的类型的成员变量绑定,这种绑定属于静态绑定,因为在编译阶段已经做了绑定。例如animal.nameanimal.staticName都是与Animal类进行绑定。

强制对象类型转换

Java编译器允许子啊具有直接或简介继承关系的类之间进行类型转换。对于向下转型,必须进行强制类型转换;对于向上转型,不必使用强制类型转换。

例如,对于一个引用类型的变量,Java 编译器按照它声明的类型来处理。如果使用 animal 调用 str 和 eatMethod() 方法将会出错,如下:

1
2
animal.str = "";    // 编译出错,提示Animal类中没有str属性
animal.eatMethod(); // 编译出错,提示Animal类中没有eatMethod()方法

如果要访问 Cat 类的成员,必须通过强制类型转换,如下:

1
2
((Cat)animal).str = "";    // 编译成功
((Cat)animal).eatMethod(); // 编译成功

把 Animal 对象类型强制转换为 Cat 对象类型,这时上面两句编译成功。对于如下语句,由于使用了强制类型转换,所以也会编译成功,例如:

1
Cat cat = (Cat)animal;    // 编译成功,将Animal对象类型强制转换为Cat对象类型

类型强制转换时想运行成功就必须保证父类引用指向的对象一定是该子类对象,最好使用 instanceof 运算符判断后,再强转,例如:

1
2
3
4
5
Animal animal = new Cat();
if (animal instanceof Cat) {
Cat cat = (Cat) animal; // 向下转型
...
}

子类的对象可以转换成父类类型,而父类的对象实际上无法转换为子类类型。因为通俗地讲,父类拥有的成员子类肯定也有,而子类拥有的成员,父类不一定有。因此,对于向上转型,不必使用强制类型转换。例如:

1
2
Cat cat = new Cat();
Animal animal = cat; // 向上转型,不必使用强制类型转换

如果两种类型之间没有继承关系,那么将不允许进行类型转换。例如:

1
2
Dog dog = new Dog();
Cat cat = (Cat)dog; // 编译出错,不允许把Dog对象类型转换为Cat对象类型

Java中为什么使用向上转型而不直接创建子类对象?

例1

定义父类 Animal,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Animal {
public void sleep() {
System.out.println("小动物在睡觉");
}
public static void doSleep(Animal animal) {
// 此时的参数是父类对象,但是实际调用时传递的是子类对象,就是向上转型。
animal.sleep();
}
public static void main(String[] args) {
animal.doSleep(new Cat());
animal.doSleep(new Dog());
}
}

子类 Cat 代码如下:

1
2
3
4
5
6
public class Cat extends Animal {
@Override
public void sleep() {
System.out.println("猫正在睡觉");
}
}

子类 Dog 代码如下:

1
2
3
4
5
6
public class Dog extends Animal {
@Override
public void sleep() {
System.out.println("狗正在睡觉");
}
}

输出结果为:

1
2
猫正在睡觉
狗正在睡觉

如果不用向上转型则必须写两个 doSleep 方法,一个传递 Cat 类对象,一个传递 Dog 类对象。这还是两个子类,如果有多个子类就要写很多相同的方法,造成重复。可以看出向上转型更好的体现了类的多态性,增强了程序的间接性以及提高了代码的可扩展性。当需要用到子类特有的方法时可以向下转型,这也就是为什么要向下转型。

比如设计一个如雷FileRead用来读取文件,ExcelRead类和WordRead类继承FileRead类。在使用程序的时候,往往事先不知道我们要读入的是Excel还是Word。所有我们向上转型用父类去接收,然后再父类中实现自动绑定,这样无论传进来的是Excel还是Word都能够完成文件读取。

总结如下:

  1. 把子类对象直接赋给父类引用是向上转型,向上转型自动转换。如 Father father = new Son();
  2. 指向子类对象的父类引用赋给子类引用是向下转型,要强制转换。使用向下转型,必须先向上转型,为了安全可以用 instanceof 运算符判断。 如 father 是一个指向子类对象的父类引用,把 father 赋给子类引用 son,即Son son =(Son)father;。其中 father 前面的(Son)必须添加,进行强制转换。
  3. 向上转型不能使用子类特有的属性和方法,只能引用父类的属性和方法,但是子类重写父类的方法是有效的。
  4. 向上转型时会优先使用子类中重写父类的方法,如例 1 中调用的 sleep 方法。
  5. 向上转型的作用是减少重复代码,可以将父类作为参数,这样使代码变得简洁,也更好的体现了多态。

Java方法重载

如果一个类中包含了两个或两个以上方法名相同的方法,但形参列表不同,这种情况被称为方法重载(overload)。

方法重载要求的是两同一不同:同一个类中的方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等等,与方法重载无关。

Java方法重写

在子类中如果创建了一个与父类中相同名称相同返回值类型相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方法被称为方法重写(override),又称为方法覆盖。当父类中的方法无法满足子类需求或子类具有特有功能的时候,需要方法重写。

重写方法遵循的规则:

  • 参数列表必须完全与被重写的方法参数列表相同。
  • 返回的类型必须与被重写的方法的返回类型相同(Java 1.5 版本之前返回值类型必须一样,之后的 Java 版本放宽了限制,返回值类型必须小于或者等于父类方法的返回值类型)。
  • 访问权限不能比父类中被重写方法的访问权限更低(public>protected>default>private)。
  • 重写方法一定不能抛出新的检査异常或者比被重写方法声明更加宽泛的检査型异常。例如,父类的一个方法声明了一个检査异常 IOException,在重写这个方法时就不能抛出 Exception,只能拋出 IOException 的子类异常,可以抛出非检査异常。

需要另外注意的几条规则:

  • 重写的方法可以用@Override注释来标识。
  • 父类的成员方法只能被它的子类重写。
  • 声明为final的方法不能被重写,但能够再次声明。
  • 声明为static的方法不能被重写,但能够再次声明。
  • 构造方法不能被重写。
  • 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为private和final的方法。
  • 子类和父类不再同一个包中时,子类只能重写父类声明为public和protected的非final方法。
  • 如果不能继承一个方法,则不能重写这个方法。

如果子类中创建了一个成员变量,而该变量的类型和名称都与父类中的同名成员变量相同,我们则称作变量隐藏。

Java多态性

多态性时面向对象编程的有一个重要特征,它指的是在父类中定义的属性和方法被子类继承后,可以具有不同的数据类型或表现出不同的行为。

对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要指定是方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译这种货会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它通过动态绑定来实现,也就是大家口中常说的多态性。

Java实现多态的三个必要条件:继承、重写和向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类。
  • 重写:子类对父类中的某些方法进行重写定义,在调用这些方法时就会调用子类的方法。
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。

Java instanceof关键字

严格来说,instanceof是Java中的一个双目运算符,由于它是由字母组成的,所以也是Jaava的保留关键字。在Java中可以使用instanceof关键字俩判断一个对象是否为一个类(或接口、抽象类、父类)的实例,语法格式如下:

1
boolean result = obj instanceof Class
  • obj:对象
  • Class:一个接口或者类

当obj是class类(或接口)的实例或者子类实例,结果返回true,否则返回false。

下面介绍Java instanceof 关键字的几种用法

  1. 声明一个class类的对象,判断obj是否为欸class类的实例对象,如以下所示:

    1
    2
    Integer integer = new Integer(1);
    System.out.println(integer instance Integer); // true
  2. 声明一个class接口实现类的对象obj,判断obj是否为class接口实现类的实例对象,如下代码所示:

    Java集合中的List接口有个典型实现类ArrayList。

    1
    2
    public class ArrayList<E> extends AbstractList<E>
    implement List<E>,RandomAccess,Cloneablle,java.io.Serializable

    所以我们可以用instanceof运算符判断ArrayList类的对象是否属于List接口的实例,如果是返回true,否则返回false。

    1
    2
    ArrayList arrayList = new ArrayList();
    System.out.println(arratList instanceof List); // true

    反过来也是返回true

    1
    2
    List list = new ArrayList();
    System.out.println(list instanceof ArrayList; // true
  3. obj是class类的直接或间接子类

    我们新建一个父类Person.class

    1
    public class Person{}

    创建Person的子类Man

    1
    public class Man extends Person{}

    测试代码如下

    1
    2
    3
    4
    5
    6
    Person p1 = new Person();
    Person p2 = new Man();
    Man m1 = new Man();
    System.out.println(p1 instanceof Man); // false
    System.out.println(p2 instanceof Man); // true
    System.out.println(m1 instanceof Man); // true

    第 4 行代码中,Man 是 Person 的子类,Person 不是 Man 的子类,所以返回结果为 false。

    值得注意的是obj必须为引用类型,不能为基本类型

    1
    2
    3
    int i = 0;
    System.out.println(i insatnce Integer); // 编译不通过
    Syetem.out.println(i instance Object); // 编译不通过

    所以,instanceof运算符只能用作对象的判断。

    当obj为null时,直接返回false,因为null没有引用任何对象。

    1
    2
    Integer i = null;
    System.out.println(i instanceof Integer); //false

    当class为null时,会发生编译错误,错误信息如下:

    1
    Syntax error on token "null", invalid ReferenceType

    所以class只能是类或者接口。

总的来说,obj instanceof Class可以理解成以下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
boolean result;
if (obj == null) {
result = false; // 当obj为null时,直接返回false
} else {
try {
// 判断obj是否可以强制转换为T
T temp = (T) obj;
result = true;
} catch (ClassCastException e) {
result = false;
}
}

当obj能够被强制类型转换为Class时,返回true,否则返回false

Java抽象类

Java提供了具体类和抽象类两种,之前学的都是具体类,也就是能够完全的描绘出一个对象的类,如果不能完全描绘出一个类的具体行为,这样的类被称为抽象类,抽象类的语法格式如下:

1
2
3
<abstract>class<class_name>{
<abstract><type><method_name>(parameter-list);
}
  • abstract:表示该类或该方法是抽象的
  • class_name:表示抽象类的名称
  • method_name:表示抽象方法名
  • parameter-list:表示方法列表参数

如果一个方法用abstract关键字修饰,则说明该方法是抽象方法,抽象方法中只有声明没有实现。需要注意的是abstract关键字只能用于普通方法,不能用于static方法或构造方法。

抽象方法的三个特性:

  1. 抽象方法没有主体
  2. 抽象方法必须存在于抽象类中
  3. 子类重写父类时,必须重写父类的所有抽象方法,否则子类还是抽象类

主要:在使用abstract关键字修饰抽象方法时不能使用private修饰,因为抽象方法必须被子类重写,而如果使用了private声明,则子类是无法重写的。

抽象类的定义和使用规则如下:

  1. 抽象类和抽象方法都要使用abstract关键字声明
  2. 如果以方法被声明为抽象的,那么这个类也必须声明为抽象的。而一个抽象类中,可以有0n个抽象方法,已经0n个具体方法
  3. 抽象类不能被实例化,也就是不能用new关键字创建对象

Java接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行的更加彻底,可以提炼出一种更特殊的“抽象类”————接口(Interface)。接口可以被理解为一种特殊的类,不同的是接口成员没有执行体,是由全局常量和公共抽象方法组成。

定义接口

接口定义的语法格式如下:

1
2
3
4
5
[public] interface interface_name [extends interface1_name[,interface_name2,...]]{
// 接口体,其中可以包含定义常量和声明方法
[public] [static] [final] type constant_name = value; // 定义常量
[public] [abstract] returnType method_name(parameter_list); // 声明方法
}
  • public表示接口访问修饰符,当没有修饰符时,则使用默认的修饰符,此时该接口的访问权限金属与所属的包
  • interface_name表示接口的名称。接口名因与类名采用相同的命名规则,即如果仅从语法的角度来看,接口名只要是合法的标识符即可,如遵从可读性规范,则应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无需任何分隔符
  • extends表示接口的继承关系
  • interface1_name表示要继承的接口名称
  • constant_name表示返回值类型,一般是static和final型
  • returnType表示方法的返回值类型
  • parameter_list表示参数列表,在接口中时没有方法体的

注意:一个接口可以由多个直接父接口,但接口只能继承接口,不能继承类

接口的限制

  • 具有public的访问控制符接口,允许任何类使用;没有指定public的接口,其访问将局限于所属包。

  • 方法的声明不需要其它修饰符,在接口中声明的方法,将隐式地声明为公有的(public)和抽象的(abstract)。

  • 在Java接口中声明的变量其实都是常量,接口中的变量声明,将隐式地声明为public、static、和final,即常量,所以接口中定义的变量必须初始化。

  • 接口没有构造方法,不能被实例化,例如:

    1
    2
    3
    public interface A{
    public A(){...} // 编译出错,接口不允许定义构造方法
    }

    一个接口不能实现另一个接口,但它能继承多个其它接口。子接口可以对父接口的方法和变量进行重写。例如:

    1
    2
    3
    4
    5
    public interface StudentInterface extends PeopleInterface{
    // 接口StudentInterface继承PeopleInterface
    int age = 25; // 常量age重写父接口中的age常量
    void getInfo(); // 方法getInfo()重写父接口中的getInfo()方法
    }

    例如定义一个接口MyInterface,并在该接口中声明常量和方法:

    1
    2
    3
    4
    5
    public interface MyInterface{
    String name; // 不合法,变量name必须初始化
    int age = 20; // 合法,等同于public static final int age = 20;
    void getInfo(); // 方法等同于 public abstract void getInfo();
    }

实现接口

接口的主要用途就是被实现类实现,一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也为Java单继承灵活性不足作的补充。类实现接口的的语法格式如下:

1
2
3
<public> class <class_name> [extends superclass_name] [implements interface1_name[,interface2_name...]]{
// 主体
}
  • public:类的修饰符
  • superclass_name:需要继承的父类名称
  • interface1_name:需要实现的接口名称

接口实现需要注意以下几点:

  • 实现接口与继承父类相似,一样可以获得接口里定义的常量和方法。如果一个类需要实现多个接口,则多个接口之间可以用逗号分隔。
  • 一个类可以继承一个父类,并实现多个接口,implements部分必须放在extends部分之后。
  • 一个类实现类一个后多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口哪里继承到的抽象方法,该列也必须定义成抽象类。

Java抽象类和接口的联系和区别

1)抽象类

在 Java 中,被关键字 abstract 修饰的类称为抽象类;被 abstract 修饰的方法称为抽象方法,抽象方法只有方法声明没有方法体。

抽象类有以下几个特点:

  1. 抽象类不能被实例化,只能被继承。
  2. 包含抽象方法的类一定是抽象类,但抽象类不一定包含抽象方法(抽象类可以包含普通方法)。
  3. 抽象方法的权限修饰符只能为 public、protected 或 default,默认情况下为 public。
  4. 一个类继承于一个抽象类,则子类必须实现抽象类的抽象方法,如果子类没有实现父类的抽象方法,那子类必须定义为抽象类。
  5. 抽象类可以包含属性、方法、构造方法,但构造方法不能用来实例化对象,只能被子类调用。

2)接口

接口可以看成是一种特殊的类,只能用 interface 关键字修饰。

Java 中的接口具有以下几个特点:

  1. 接口中可以包含变量和方法,变量被隐式指定为 public static final,方法被隐式指定为 public abstract(JDK 1.8 之前)。
  2. 接口支持多继承,即一个接口可以继承(extends)多个接口,间接解决了 Java 中类不能多继承的问题。
  3. 一个类可以同时实现多个接口,一个类实现某个接口则必须实现该接口中的抽象方法,否则该类必须被定义为抽象类。

3)抽象类和接口的区别

接口和抽象类很像,它们都具有如下特征。

  • 接口和抽象类都不能被实例化,主要用于被其他类实现和继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

接口和抽象类在用法上也存在差别,如下表所示:

参数 抽象类 接口
实现 子类使用 extends 关键字来继承抽象类,如果子类不是抽象类,则需要提供抽象类中所有声明的方法的实现。 子类使用 implements 关键字来实现接口,需要提供接口中所有声明的方法的实现。
访问修饰符 可以用 public、protected 和 default 修饰 默认修饰符是 public,不能使用其它修饰符
方法 完全可以包含普通方法 只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现
变量 既可以定义普通成员变量,也可以定义静态常量 只能定义静态常量,不能定义普通成员变量
构造方法 抽象类里的构造方法并不是用于创建对象,而是让其子类调用这些构造方法来完成属于抽象类的初始化操作 没有构造方法
初始化块 可以包含初始化块 不能包含初始化块
main 方法 可以有 main 方法,并且能运行 没有 main 方法
与普通Java类的区别 抽象类不能实例化,除此之外和普通 Java 类没有任何区别 是完全不同的类型
运行速度 比接口运行速度要快 需要时间去寻找在类种实现的方法,所以运行速度稍微有点慢

表9-0 接口和抽象类的区别
一个类最多只能有一个直接父类,包括抽象类,但一个类可以直接实现多个接口,通过实现多个接口可以弥补 Java 单继承的不足。

4)抽象类和接口的应用场景

抽象类的应用场景:

  1. 父类只知道其子类应该包含怎样的方法,不能准确知道这些子类如何实现这些方法的情况下,使用抽象类。
  2. 从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为子类的模板,从而避免了子类设计的随意性。

接口的应用场景:

  1. 一般情况下,实现类和它的抽象类之前具有 “is-a” 的关系,但是如果我们想达到同样的目的,但是又不存在这种关系时,使用接口。
  2. 由于 Java 中单继承的特性,导致一个类只能继承一个类,但是可以实现一个或多个接口,此时可以使用接口。

什么时候使用抽象类和接口:

  • 如果拥有一些方法并且想让它们有默认实现,则使用抽象类。
  • 如果想实现多重继承,那么必须使用接口。因为 Java 不支持多继承,子类不能继承多个类,但可以实现多个接口,因此可以使用接口。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果使用接口并不断需要改变基本功能,那么就需要改变所有实现了该接口的类。

Java内部类介绍

在类内部可以定义成员变量和方法,且在类内部也可以定义另一个类。如果在类Outer的内部再定义一个类Inner,此时类Inner就成为内部类(或称为嵌套类),而类Outer则称为外部类(或者宿主类)。

内部类可以很好的实现隐藏,因为内部类可以使用外部类无法使用的private与protected权限。且内部类拥有外部类所有元素的访问权限。

内部类分类

内部类分为:实例内部类、静态内部类和成员内部类

在类A中定义类B,那么B就是内部类,也称为嵌套类,如果在类B中在定义内部类C,那么最外层的类A称为顶层类(或者顶级类)。

Java-9-0

图9-0 内部类的分类

内部类的特点

  • 内部类仍然是一个独立的类,在编译后内部类汇编编译成独立的.class文件,但是前面冠以外部类的类名和$符号。
  • 内部类不能用普通的方式访问。内部类时外部类的一个成员,因此,内部类可以自由地访问外部类的成员变量,无论是否为private的。
  • 内部类声明为静态的,就不能随便访问外部类的成员变量,仍然是只能访问外部类的静态成员变量。

下面给出内部类最简单的应用:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test{
public class InnerClass{
public int getSum(int x,int y){
return x + y;
}
}
public static void main(String[] args){
Test.InnerClass ti = new Test().new InnerClass();
int i = ti.getSum(2,3);
System.out.println(i); // 输出5
}
}

有关内部类的说明有如下几点。

  • 外部类只有两种访问级别:public 和默认;内部类则有 4 种访问级别:public、protected、 private 和默认。

  • 在外部类中可以直接通过内部类的类名访问内部类。

    1
    InnerClass ic = new InnerClass();    // InnerClass为内部类的类名
  • 在外部类意外的其它类中则需要提供内部类的完整类名访问内部类。

    1
    Test.InnerClass ti = newTest().new InnerClass();    // Test.innerClass是内部类的完整类名
  • 内部类与外部类不能重名

Tip:内部类的很多访问规则可以参考变量和方法。另外内部类可以使重写结构变得更紧凑,但是却在一定程度上破坏了Java面向对象的思想。

Java实例内部类

实例内部类值得是没有用static修饰的内部类,有道地方也称为非静态内部类。

1
2
3
4
5
public class Outer{
class Inner{
// 实例内部类
}
}

1)在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Outer {
class Inner1 {
}
Inner1 i = new Inner1(); // 不需要创建外部类实例
public void method1() {
Inner1 i = new Inner1(); // 不需要创建外部类实例
}
public static void method2() {
Inner1 i = new Outer().new inner1(); // 需要创建外部类实例
}
class Inner2 {
Inner1 i = new Inner1(); // 不需要创建外部类实例
}
}
class OtherClass {
Outer.Inner i = new Outer().new Inner(); // 需要创建外部类实例
}

2)在实例内部类中,可以访问外部类的所有成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Outer {
public int a = 100;
static int b = 100;
final int c = 100;
private int d = 100;
public String method1() {
return "实例方法1";
}
public static String method2() {
return "静态方法2";
}
class Inner {
int a2 = a + 1; // 访问public的a
int b2 = b + 1; // 访问static的b
int c2 = c + 1; // 访问final的c
int d2 = d + 1; // 访问private的d
String str1 = method1(); // 访问实例方法method1
String str2 = method2(); // 访问静态方法method2
}
public static void main(String[] args) {
Inner i = new Outer().new Inner(); // 创建内部类实例
System.out.println(i.a2); // 输出101
System.out.println(i.b2); // 输出101
System.out.println(i.c2); // 输出101
System.out.println(i.d2); // 输出101
System.out.println(i.str1); // 输出实例方法1
System.out.println(i.str2); // 输出静态方法2
}
}

提示:如果有多层嵌套,则内部类可以访问所有外部类的成员。

3)在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类 A 包含内部类 B,类 B 中包含内部类 C,则在类 A 中不能直接访问类 C,而应该通过类 B 的实例去访问类 C。

4)外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例。

如果实例内部类 B 与外部类 A 包含有同名的成员 t,则在类 B 中 t 和 this.t 都表示 B 中的成员 t,而 A.this.t 表示 A 中的成员 t。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Outer {
int a = 10;

class Inner {
int a = 20;
int b1 = a;
int b2 = this.a;
int b3 = Outer.this.a;
}

public static void main(String[] args) {
Inner i = new Outer().new Inner();
System.out.println(i.b1); // 输出20
System.out.println(i.b2); // 输出20
System.out.println(i.b3); // 输出10
}
}

5)在实例内部类中不能定义 static 成员,除非同时使用 final 和 static 修饰。

Java静态内部类

静态内部类是指使用static修饰的内部类。

1
2
3
4
5
public class Outer{
static class Inner{
// 静态内部类
}
}

静态内部类的特点如下

1)在创建静态内部类的实例时,不需要创建外部类的实例。

1
2
3
4
5
6
public class Outer {
static class Inner {}
}
class OtherClass {
Outer.Inner oi = new Outer.Inner();
}

2)静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。

1
2
3
4
5
6
7
8
9
10
11
public class Outer {
static class Inner {
int a = 0; // 实例变量a
static int b = 0; // 静态变量 b
}
}
class OtherClass {
Outer.Inner oi = new Outer.Inner();
int a2 = oi.a; // 访问实例成员
int b2 = Outer.Inner.b; // 访问静态成员
}

3)静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。

1
2
3
4
5
6
7
8
9
public class Outer {
int a = 0; // 实例变量
static int b = 0; // 静态变量
static class Inner {
Outer o = new Outer;
int a2 = o.a; // 访问实例变量
int b2 = b; // 访问静态变量
}
}

Java局部内部类

局部内部类指的是在一个方法中定义的内部类

1
2
3
4
5
6
7
public class Test{
public void method(){
class Inner{
// 局部内部类
}
}
}

局部内部类的特点如下

1)局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰。

2)局部内部类只在当前方法中有效。

1
2
3
4
5
6
7
8
9
public class Test {
Inner i = new Inner(); // 编译出错
Test.Inner ti = new Test.Inner(); // 编译出错
Test.Inner ti2 = new Test().new Inner(); // 编译出错
public void method() {
class Inner{}
Inner i = new Inner();
}
}

3)局部内部类中不能定义 static 成员。

4)局部内部类中还可以包含内部类,但是这些内部类也不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰

5)在局部内部类中可以访问外部类的所有成员。

6)在局部内部类中只可以访问当前方法中 final 类型的参数与变量。如果方法中的成员与外部类中的成员同名,则可以使用 <OuterClassName>.this.<MemberName> 的形式访问外部类中的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
int a = 0;
int d = 0;
public void method() {
int b = 0;
final int c = 0;
final int d = 10;
class Inner {
int a2 = a; // 访问外部类中的成员
// int b2 = b; // 编译出错
int c2 = c; // 访问方法中的成员
int d2 = d; // 访问方法中的成员
int d3 = Test.this.d; //访问外部类中的成员
}
Inner i = new Inner();
System.out.println(i.d2); // 输出10
System.out.println(i.d3); // 输出0
}
public static void main(String[] args) {
Test t = new Test();
t.method();
}
}

Java匿名类

匿名类指的是没有类名的内部类,必须在创建时使用new语句来声明类,语法格式如下:

1
2
3
new <类或接口>(){
// 类的主题
};

这种形式的new语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现给定的一个接口,使用匿名类可以使代码更加简介、紧凑,模块化程度更高。

匿名类有两种实现方法:

  • 继承一个类,重写其方法。
  • 实现一个接口(可以是多个),实现其方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Out{
void show(){
System.out.println("调用Out类的show()方法");
}
}

public class TestAnontmousInterClass{
// 在这个方法中构造一个匿名内部类
private void show(){
Out anonyInter = new Out(){
// 获取匿名内部类的实例
void show(){
System.out.println("调用匿名类中的show()方法");
}
};
anonyInter.show();
}
public static void main(String[] args){
TestAnontmousInterClass() test = new TestAnontmousInterClass();
test.show();
}
}

程序的输出结果如下:

1
调用匿名类中的 show() 方法

从输出结果可以看出,匿名内部类有自己的实现。

提示:匿名内部类实现一个接口的方式与实现一个类的方式相同,这里不再赘述。

匿名类的特点如下:

1)匿名类和局部内部类一样,可以访问外部类的所有成员。如果匿名类位于一个方法中,则匿名类只能访问方法中 final 类型的局部变量和参数。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int a = 10;
final int b = 10;
Out anonyInter = new Out() {
void show() {
// System.out.println("调用了匿名类的 show() 方法"+a); // 编译出错
System.out.println("调用了匿名类的 show() 方法"+b); // 编译通过
}
};
anonyInter.show();
}

从 Java 8 开始添加了 Effectively final 功能,在 Java 8 及以后的版本中代码第 6 行不会出现编译错误。

2)匿名类中允许使用非静态代码块进行成员初始化操作。

1
2
3
4
5
6
7
8
Out anonyInter = new Out() {
int i; { // 非静态代码块
i = 10; //成员初始化
}
public void show() {
System.out.println("调用了匿名类的 show() 方法"+i);
}
};

3)匿名类的非静态代码块会在父类的构造方法之后被执行。

Java使用内部类实现多重继承

Java为了保证数据安全,只允许单继承

但由于系统在某些问题下必须使用多继承,例如遗传,我们既继承了父亲的行为和特征,也继承了母亲的行为和特征。

为了解决此问题,Java提供了两种方法让我们实现多重继承:接口和内部类。

下面给出儿子(或女儿)如何利用多继承来继承父母基因的案例。

1)创建 Father 类,在该类中添加 strong() 方法。代码如下:

1
2
3
4
5
6
public class Father {
public int strong() {
// 强壮指数
return 9;
}
}

2)创建 Mother 类,在该类中添加 kind() 方法。代码如下:

1
2
3
4
5
6
public class Mother {
public int kind() {
// 友好指数
return 8;
}
}

3)重点在于儿子类的实现,创建 Son 类,在该类中通过内部类实现多重继承。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Son {
// 内部类继承Father类
class Father_1 extends Father {
public int strong() {
return super.strong() + 1;
}
}
class Mother_1 extends Mother {
public int kind() {
return super.kind() - 2;
}
}
public int getStrong() {
return new Father_1().strong();
}
public int getKind() {
return new Mother_1().kind();
}
}

上述代码定义两个内部类,这两个内部类分别继承 Father(父亲)类和 Mother(母亲)类,且都可以获取各自父类的行为。这是内部类一个很重要的特性:内部类可以继承一个与外部类无关的类,从而保证内部类的独立性。正是基于这一点,多重继承才会成为可能。

4)创建 Test 类进行测试,在 main() 方法中实例化 Son 类的对象,然后分别调用该对象的 getStrong() 方法和 getKind() 方法。代码如下:

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
Son son = new Son();
System.out.println("Son 的强壮指数:" + son.getStrong());
System.out.println("Son 的友好指数:" + son.getKind());
}
}

执行上述代码,输出结果如下:

1
2
Son 的强壮指数:10
Son 的友好指数:6

Java8新特性:Effectively final

Java中局部内部类和匿名内部类访问的局部变量必须由final修饰,以保证内部类数据和外部类数据一致。但从Java 8 开始,我们可以不加final修饰符,由系统默认添加,这在Java 8 之前的版本是不允许的,Java将这个功能称为Effectively final

编写同样的代码,,分别在Java 7 和Java 8下运行:

1
2
3
4
5
6
7
8
9
10
11
public class Test{
public static void main(String[] args){
String name = "C语言中文网";
new Runnable(){
@Override
public void run(){
System.out.println(name);
}
}
}
}

图 9-1 是 Java 7 编译结果,图 9-2 和图 9-3 是 Java 8 编译结果。

java-9-1

图9-1 Java 7 编译结果

可以看到在 Java 7(图 1)中出现代码错误,提示我们必须显式的声明这个变量为 final 的(run 方法中代码为输出 name 语句,即System.out.println(name);)。

java-9-2

图9-2 Java 8 编译结果1

java-9-3

图9-3 Java 8 编译结果2

因为系统会默认添加 final 修饰符,所以在图 9-2 和图 9-3 中可以在匿名内部类中直接使用非 final 变量,而 final 修饰的局部变量不能在被重新赋值,所以图 9-3 中出现编译错误。也就是说从 Java 8 开始,它不要求程序员必须将访问的局部变量显式的声明为 final 的。只要该变量不被重新赋值就可以。

一个非final的局部比那辆或方法参数,其值在初始化后就未更改,那么该变量就是effectively final。在Lambda表达式中,使用局部变量的时候,也要求该变量必须是final的,所有effectively final在Lambda表达式上下文中非常有用。

Java Lambda表达式

Lambda表达式(Lambda expression)是一个匿名函数,也可称为闭包(Closure)。

Lambda表达式是推掉 Java 8 发布的重要新特性,它允许把函数作为一个方法的参数,下面给出案例。

先定义一个计算数值的接口

1
2
3
4
public interface Calculable{
// 计算两个int数值
int calculateInt(int a,int b);
}

Calculable接口只有一个方法calculateInt,参数是两个int类型,返回值也是int类型。实现方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test{

/**
* 提供操作符,进行计算
*
* @param opr 操作符
* @return 实现Calculable接口对象
*/
public static Calculable calculate(char opr){
Calculable result;
if(opr == '+'){
// 匿名内部类实现Calculable接口
result = new Calculable(){
// 实现加法运算
@Override
public int calculateInt(int a,int b){
return a + b;
}
};
}else{
// 匿名内部类实现Calculable接口
result = new Calculable(){
//实现减法运算
@Override
public int clculateInt(int a,int b){
return a - b;
}
};
}
return result;
}
}

方法 calculate 中 opr 参数是运算符,返回值是实现 Calculable 接口对象。代码第 13 行和第 23 行都采用匿名内部类实现 Calculable 接口。代码第 16 行实现加法运算。代码第 26 行实现减法运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 实现加法计算Calculable对象
Calculable f1 = calculate('+');
// 实现减法计算Calculable对象
Calculable f2 = calculate('-');
// 调用calculateInt方法进行加法计算
System.out.println(n1 + "+" + n2 + "=" + f1.calculateInt(n1, n2));
// System.out.printf("%d + %d = %d \n", n1, n2, f1.calculateInt(n1, n2));
// 调用calculateInt方法进行减法计算
System.out.println(n1 + "-" + n2 + "=" + f1.calculateInt(n1, n2));
// System.out.printf("%d - %d = %d \n", n1, n2, f2.calculateInt(n1, n2));

代码第 5 行中 f1 是实现加法计算 Calculable 对象,代码第 7 行中 f2 是实现减法计算 Calculable 对象。代码第 9 行和第 12 行才进行方法调用。

上述代码中列出了两种输出方式,下面简单介绍一下 Java 中常见的输出函数:

  1. printf 主要继承了C语言中 printf 的一些特性,可以进行格式化输出。
  2. print 就是一般的标准输出,但是不换行。
  3. println 和 print 基本没什么差别,就是最后会换行。

前面例子中使用内部类的方法calculate很臃肿,Java 8 采用了Lambda表达式可以代替内部类。修改后的通用方法calculate代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 通过操作符,进行计算
* @param opr 操作符
* @return 实现Calculable接口对象
*/
public static Calculable calculate(char opr){
Calculable result;
if(opr == '+'){
// Lambda表达式实现Calculable接口
result = (int a,int b) -> {
return a + b;
};
}else{
// Lambda表达式实现Calculable接口
result = (int a,int b) -> {
return a - b;
};
}
return result;
}

代码第 10 行和第 15 行用 Lambda 表达式替代匿名内部类,可见代码变得简洁。通过以上示例我们发现,Lambda 表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。

Lambda表达式语法

Lambda的表达式标准语法如下:

1
2
3
(参数列表) -> {
// Lambda表达式体
}

->被称为箭头操作符或Lambda操作符,它将Lambda表达式拆分成两部分:

  • 左侧:Lambda表达式的参数列表
  • 右侧:Lambda表达式的结构体

Java Lambda表达式的优缺点

优点:

  1. 代码简洁,开发迅速
  2. 方便函数式编程
  3. 非常容易进行并行计算
  4. Java 引入 Lambda,改善了集合操作(引入 Stream API)

缺点:

  1. 代码可读性变差
  2. 在非并行计算中,很多计算未必有传统的 for 性能要高
  3. 不容易进行调试

函数式接口

Lambda表达式实现的接口不是普通的接口,而是函数式接口。如果一个接口中有且只有一个抽象方法(Object类的方法不包括在内),那这个接口就可以被当作函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么Lambda表达式会发生编译错误。

The target type of this expression must be a functional interface

这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface,示例代码如下

1
2
3
4
5
6
// 可计算接口
@FunctionalInterface
public interface Calculable{
// 计算两个int数值
int calculateInt(int a,int b);
}

这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface,示例代码如下

提示:Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的。

Java Lambda表达式的3种简写方式

省略参数类型

Lambda 表达式可以根据上下文环境推断出参数类型。calculate 方法中 Lambda 表达式能推断出参数 a 和 b 是 int 类型,简化形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// Lambda表达式实现Calculable接口
result = (a, b) -> {
return a + b;
};
} else {
// Lambda表达式实现Calculable接口
result = (a, b) -> {
return a - b;
};
}
return result;
}

代码第 5 行和第 10 行是《Java Lambda表达式》一节示例中result = (int a, int b) -> {代码的简化写法,其中 a 和 b 是参数。

省略参数小括号

如果 Lambda 表达式中的参数只有一个,可以省略参数小括号。修改 Calculable 接口中的 calculateInt 方法,代码如下。

1
2
3
4
5
6
// 可计算接口
@FunctionalInterface
public interface Calculable {
// 计算一个int数值
int calculateInt(int a);
}

其中 calculateInt 方法只有一个 int 类型参数,返回值也是 int 类型。调用 calculateInt 方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String[] args) {
int n1 = 10;
// 实现二次方计算Calculable对象
Calculable f1 = calculate(2);
// 实现三次方计算Calculable对象
Calculable f2 = calculate(3);
// 调用calculateInt方法进行加法计算
System.out.printf("%d二次方 = %d \n", n1, f1.calculateInt(n1));
// 调用calculateInt方法进行减法计算
System.out.printf("%d三次方 = %d \n", n1, f2.calculateInt(n1));
}
/**
* 通过幂计算
*
* @param power 幂
* @return 实现Calculable接口对象
*/
public static Calculable calculate(int power) {
Calculable result;
if (power == 2) {
// Lambda表达式实现Calculable接口
// 标准形式
result = (int a) -> {
return a * a;
};
} else {
// Lambda表达式实现Calculable接口
// 省略形式
result = a -> {
return a * a * a;
};
}
return result;
}

上述代码第 25 行和第 31 行都是实现 Calculable 接口的 Lambda 表达式。代码第 25 行是标准形式没有任何的简化。代码第 31 行省略了参数类型和小括号。

输出结果为:

10二次方 = 100
10三次方 = 1000

省略return和大括号

如果 Lambda 表达式体中只有一条语句,那么可以省略 return 和大括号,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Calculable calculate(int power) {
Calculable result;
if (power == 2) {
// Lambda表达式实现Calculable接口
// 标准形式
result = (int a) -> {
return a * a;
};
} else {
// Lambda表达式实现Calculable接口
// 省略形式
result = a -> a * a * a;
}
return result;
}

上述代码第 12 行是省略了 return 和大括号,这是最简化形式的 Lambda 表达式了,代码太简洁了,但是对于初学者而言很难理解这个表达式。

Java Lambda表达式的使用

本节介绍Lambda表达式的集中使用方式

作为参数使用Lambda表达式

Lambda表达式一种常见的用途就是作为参数传递给方法,这需要声明参数的类型声明为函数式接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args){
int n1 = 10;
int n2 = 5;
// 打印加法计算结果
display((a,b) -> {
return a + b;
},n1,n2);
// 打印减法计算结果
display((a,b) -> a - b,n1,n2);
}


/**
* 打印计算结果
*
* @param calc Lambda表达式
* @param n1 操作数1
* @param n2 操作数2
*/
public static void display(Calculable calc,int n1,int n2){
System.out.println(calc.calculateInt(n1,n2));
}

上述代码第 19 行定义 display 打印计算结果方法,其中参数 calc 类型是 Calculable,这个参数即可以接收实现 Calculable 接口的对象,也可以接收 Lambda 表达式,因为 Calculable 是函数式接口。 代码第 7 行和第 9 行两次调用 display 方法,它们第一个参数都是 Lambda 表达式。

访问变量

Lambda表达式课访问所在外层作用域的变量,包括成员变量和局部变量。

访问成员变量

成员变量包括实例成员变量和静态成员变量。在Lambda表达式中可以访问这些成员变量,此时Lambda表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LambdaDemo {
// 实例成员变量
private int value = 10;
// 静态成员变量
private static int staticValue = 5;

// 静态方法,进行加法运算
public static Calculable add() {
Calculable result = (int a, int b) -> {
// 访问静态成员变量,不能访问实例成员变量
staticValue++;
int c = a + b + staticValue;
// this.value;
return c;
};
return result;
}

// 实例方法,进行减法运算
public Calculable sub() {
Calculable result = (int a, int b) -> {
// 访问静态成员变量和实例成员变量
staticValue++;
this.value++;
int c = a - b - staticValue - this.value;
return c;
};
return result;
}
}

LambdaDemo 类中声明一个实例成员变量 value 和一个静态成员变量 staticValue。此外,还声明了静态方法 add(见代码第 8 行)和实例方法 sub(见代码第 20 行)。add 方法是静态方法,静态方法中不能访问实例成员变量,所以代码第 13 行的 Lambda 表达式中也不能访问实例成员变量,也不能访问实例成员方法。

sub 方法是实例方法,实例方法中能够访问静态成员变量和实例成员变量,所以代码第 23 行的 Lambda 表达式中可以访问这些变量,当然实例方法和静态方法也可以访问。当访问实例成员变量或实例方法时可以使用 this,如果不与局部变量发生冲突情况下可以省略 this。

访问局部变量

对于成员变量的访问 Lambda 表达式与普通方法没有区别,但是访问局部变量时,变量必须是 final 类型的(不可改变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class LambdaDemo {
// 实例成员变量
private int value = 10;
// 静态成员变量
private static int staticValue = 5;

// 静态方法,进行加法运算
public static Calculable add() {
// 局部变量
int localValue = 20;
Calculable result = (int a, int b) -> {
// localValue++;
// 编译错误
int c = a + b + localValue;
return c;
};
return result;
}

// 实例方法,进行减法运算
public Calculable sub() {
// final局部变量
final int localValue = 20;
Calculable result = (int a, int b) -> {
int c = a - b - staticValue - this.value;
// localValue = c;
// 编译错误
return c;
};
return result;
}
}

上述代码第 10 行和第 23 行都声明一个局部变量 localValue,Lambda 表达式中访问这个变量,如代码第 14 行和第 25 行。不管这个变量是否显式地使用 final 修饰,它都不能在 Lambda 表达式中修改变量,所以代码第 12 行和第 26 行如果去掉注释会发生编译错误。

Lambda 表达式只能访问局部变量而不能修改,否则会发生编译错误,但对静态变量和成员变量可读可写。

方法引用

方法引用可以理解为Lambda表达式的快捷写法,但复用的地方不多。

Java 8 之后增加了双冒号::运算符,该运算符用于“方法引用”,而非调用方法。“方法引用”虽然没有直接使用Lambda表达式,但也与Lambda表达式有关,与函数式接口有关。方法引用的语法格式如下:

1
ObjectRef::methodName
  • ObjectRef:类名或者实例名
  • methodName:对应的方法名

被引用的方法的参数列表和返回值类型,必须与函数时接口方法参数列表和方法返回值类型一致,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LambdaDemo{
// 静态方法,进行加法运算
// 参数列表要与函数时接口方法calculateInt(int a,int b)兼容
public static int add(int a,int b){
return a + b;
}

// 实例方法,进行减法运算
// 参数列表要与函数式接口方法calculateInt(int a,int b)兼容
public int sub(int a,int b){
return a - b;
}
}

LambdaDemo 类中提供了一个静态方法 add,一个实例方法 sub。这两个方法必须与函数式接口方法参数列表一致,方法返回值类型也要保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HelloWorld {
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 打印加法计算结果
display(LambdaDemo::add, n1, n2);
LambdaDemo d = new LambdaDemo();
// 打印减法计算结果
display(d::sub, n1, n2);
}

/**
* 打印计算结果
*
* @param calc Lambda表达式
* @param n1 操作数1
* @param n2 操作数2
*/
public static void display(Calculable calc, int n1, int n2) {
System.out.println(calc.calculateInt(n1, n2));
}
}

代码第 19 行声明 display 方法,第一个参数 calc 是 Calculable 类型,它可以接受三种对象:Calculable 实现对象、Lambda 表达式和方法引用。代码第 6 行中第一个实际参数LambdaDemo::add是静态方法的方法引用。代码第 9 行中第一个实际参数d::sub,是实例方法的方法引用,d 是 LambdaDemo 实例。

提示:代码第 6 行的LambdaDemo::add和第 9 行的d::sub是方法引用,此时并没有调用方法,只是将引用传递给 display 方法,在 display 方法中才真正调用方法。

Java Lambda表达式与匿名内部类的联系和区别

Java Lambda 表达式的一个重要用法是简化某些匿名内部类的写法,因此它可以部分取代匿名内部类的作用。

Lambda 表达式与匿名内部类的相同点如下:

  • Lambda 表达式与匿名内部类一样,都可以直接访问 effectively final 的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
  • Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。

下面程序示范了 Lambda 表达式与匿名内部类的相似之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@FunctionalInterface
interface Displayable {
// 定义一个抽象方法和默认方法
void display();
default int add(int a, int b) {
return a + b;
}
}
public class LambdaAndInner {
private int age = 2;
private static String name = "梧席的小站";
public void test() {
String url = "https://www.wuster.store/";
Displayable dis = () -> {
// 访问的局部变量
System.out.println("url 局部变量为:" + url);
// 访问外部类的实例变量和类变量
System.out.println("外部类的 age 实例变量为:" + age);
System.out.println("外部类的 name 类变量为:" + name);
};
dis.display();
// 调用dis对象从接口中继承的add()方法
System.out.println(dis.add(3, 5));
}
public static void main(String[] args) {
LambdaAndInner lambda = new LambdaAndInner();
lambda.test();
}
}

输出结果为:

1
2
3
4
url 局部变量为:https://www.wuster.store/
外部类的 age 实例变量为:2
外部类的 name 类变量为:梧席的小站
8

上面程序使用 Lambda 表达式创建了一个 Displayable 的对象,Lambda 表达式的代码块中的代码第 19、21 和 22 行分别示范了访问“effectively final”的局部变量、外部类的实例变量和类变量。从这点来看, Lambda 表达式的代码块与匿名内部类的方法体是相同的。

与匿名内部类相似的是,由于 Lambda 表达式访问了 url 局部变量,因此该局部变量相当于有一个隐式的 final 修饰,因此同样不允许对 url 局部变量重新赋值。

当程序使用 Lambda 表达式创建了 Displayable 的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法,如上面程序代码第 26 行所示。

Lambda 表达式与匿名内部类主要存在如下区别。

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。

对于 Lambda 表达式的代码块不允许调用接口中定义的默认方法的限制,可以尝试对上面的 LambdaAndInner.java 程序稍做修改,在 Lambda 表达式的代码块中增加如下一行:

1
2
// 尝试调用接口中的默认方法,编译器会报错
System.out.println(add(3, 5));

虽然 Lambda 表达式的目标类型 Displayable 中包含了 add() 方法,但 Lambda 表达式的代码块不允许调用这个方法;如果将上面的 Lambda 表达式改为匿名内部类的写法,当匿名内部类实现 display() 抽象方法时,则完全可以调用这个 add() 方法,如下面代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void test() {
String url = "https://www.wuster.store/";
Displayable dis = new Displayable() {
@Override
public void display() {
// 访问的局部变量
System.out.println("url 局部变量为:" + url);
// 访问外部类的实例变量和类变量
System.out.println("外部类的 age 实例变量为:" + age);
System.out.println("外部类的 name 类变量为:" + name);
System.out.println(add(3, 5));
}
};
dis.display();
}
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View