《第一行代码:Java》第8章、Java新特性 读书笔记
文章目录
第8章、Java新特性
在JDK1.5中有三大主要新特性:泛型、枚举和Annotation。而JDK1.8最大新特性就是引入了Lambda表达式。
8.1 可变参数
当在Java中调用一个方法时,必须严格的按照方法定义的变量进行参数传递。但有时候并不能确定需要传入的参数数量,最早解决这个问题,往往需要将多个参数封装为数组,再传入方法中。
但是从JDK1.5 开始,为了解决传入任意个参数的问题,专门在方法定义上提供了可变参数的概念,语法格式如下:
[public|protected|private][static][final][abstract] 返回值类型 方法名称(参数类型 ... 变量) {
return 返回值;
}
开发者可以使用“参数类型 … 变量”的形式传递若干个参数,而方法在接收到这些参数之后就会把他们变成一个数组保存。
package com.yootk.demo;
public class TestDemo {
public static void main(String[] args) {
// 可变参数支持接收数组
System.out.println(add(new int[]{1,2,3})); // 传递3个整型数据
System.out.println(add(new int[]{10,20})); // 传递2个整型数据
// 或者使用“,”区分不同的参数,接收的时候还是数组
System.out.println(add(1,2,3)); // 传递3个参数
System.out.println(add(10,20)); // 传递2个参数
System.out.println(add()); // 不传递参数
}
/**
* 实现任意多个整型数据的相加操作
* @param data 由于要接收多个整型数据,所以使用数组完成接收
* @return 多个整型数据的累加结果
*/
public static int add(int ... data) {
int sum = 0 ;
for (int x = 0 ; x < data.length ; x ++) {
sum += data[x] ;
}
return sum ;
}
}
8.2 foreach循环
foreach是一种加强型的for循环操作,主要用于简化数组或集合数据的输出操作。
从JDK1.5之后,for循环可以这样使用了:
for(数据类型 变量:数组或集合){
每一次循环都会自动将数组对应索引的内容设置给变量
}
利用foreach循环实现输出:
package com.yootk.demo;
public class TestDemo { public static void main(String[] args) {
int data[] = new int[] { 1, 2, 3, 4, 5 }; // 定义数组
for (int x : data) { // 循环次数由数组长度决定
// 每一次循环实际上都表示数组的角标增长,会取得每一个数组的内容,并且将其设置给x
System.out.println(x + "、"); // x就是每一个数组元素的内容
}
}
}
8.3 静态导入
如果在某一个类中定义的方法全部属于 static 型的方法,那么在其他包中想使用这些方法,可以进行静态导入。
-
定义一个类:
package com.yootk.util; public class MyMath { public static int add(int x,int y) { return x + y ; } public static int div(int x,int y) { return x / y ; } }
-
传统情况下,想要使用其他包的static方法,首先需要导入这个包,然后使用 “类名.方法名()”去调用。
package com.yootk.demo; import com.yootk.util.MyMath; public class TestDemo { public static void main(String[] args) { System.out.println("加法操作:" + MyMath.add(10, 20)); System.out.println("除法操作:" + MyMath.div(10, 2)); } }
-
静态导入:如果一个类中的方法全为static方法,那么可以使用静态导入 “import static 包.类.*;”的方式,然后调用的时候,直接使用“方法名()”去调用。
package com.yootk.demo; // 将MyMath类中的全部static方法导入,这些方法就好比在主类中定义的static方法一样 import static com.yootk.util.MyMath.*; public class TestDemo { public static void main(String[] args) { // 直接使用方法名称访问 System.out.println("加法操作:" + add(10, 20)); System.out.println("除法操作:" + div(10, 2)); } }
8.4 泛型
在面向对象的开发中,利用对小的多态性可以解决方法参数的统一问题,但随之而来的是一个新的问题:“向下转型会存在类转换异常(ClassCastException)”,所以向下转型并不是安全的,了解决这个问题,从JDK1.5开始便提供了泛型技术。
定义泛型:
class 类名<类型标记> {}
实例化泛型:
类名<数据类型> 对象名 = new 类名<数据类型>;
泛型的引出
-
假设需要一个描述坐标的类,且要求该类能保存以下3种类型的坐标:
- 保存数字:x = 10、y = 20;
- 保存小数:x = 10.2、y = 20.3;
- 保存字符串:x = 东经20度、y = 北纬15度;
-
什么数据能保存这3种数据类型能,首先想到的是Object类型,因此此时会存在以下转换关系:
- int型:int自动装箱为Integer,Integer向上转型为Object;
- double型:double自动装箱为Double,Double向上转型为Object;
- String型:String直接向上转型为Object;
-
定义Point类,使用Object作为属性类型:
class Point { // 定义坐标 private Object x ; // 可以保存任意数据 private Object y ; // 可以保存任意数据 public Point(Object x, Object y) { this.x = x; this.y = y; } public Object getX() { return x; } public Object getY() { return y; }
-
Point类中的属性可以接收任何数据类型,为了更好说明问题,下面分别设置不同的数据类型,以测试程序。
public class TestDemo { public static void main(String[] args) { // 第一步:根据需要设置数据,假设此时的作用是传递坐标 Point p1 = new Point(10, 20) ; // 实例化Point类数据 Point p2 = new Point(10.2, 20.3) ; Point p3 = new Point("东经20度", "北纬15度") ; // 第二步:根据设置好的坐标取出数据进行操作 int x1 = (Integer) p1.getX(); // 取出坐标数据,先由Object对象强制向下转型为Integer对象,在自动拆箱为int型 int y1 = (Integer) p1.getY(); double x2 = (Double) p2.getX(); double y2 = (Double) p2.getY(); String x3 = (String) p3.getX(); String y3 = (String) p3.getY(); System.out.println("x坐标:" + x1 + ",y坐标:" + y1); System.out.println("x坐标:" + x2 + ",y坐标:" + y2); System.out.println("x坐标:" + x3 + ",y坐标:" + y3); } }
本程序对于设计的基本要求已经成功实现,而整个设计的关键在于Object类的使用。
-
但也正是Object类可以描述所有的数据类型,所以这时就会带来一个严重的后果:一旦设置的内容出问题了,在程序编译阶段是无法检查出来的。
public class TestDemo { public static void main(String[] args) { // 第一步:根据需要设置数据,假设此时的作用是传递坐标 Point p = new Point() ; // 实例化Point类数据 p.setX("东经100度"); // 设置坐标数据 p.setY(10) ; // 设置坐标数据,数据错误 // 第二步:根据设置好的坐标取出数据进行操作 String x = (String) p.getX(); // 取出坐标数据 String y = (String) p.getY(); // 取出坐标数据 System.out.println("x坐标:" + x + ",y坐标:" + y); } }
由于在设置数据时出现错误,将y坐标设置为int型,而int型也顺利地被Object接收了;但在取出来的时候,y坐标没法以String的形式进行强制向下转型,这时候就会发生“ClassCastException”异常。可是这个异常并不是在程序编译时产生的,而是在运行中出现,这样就会为开发带来很大的困扰。面对这样的问题,如果能在编译的时候排查出来才是最合理的。
-
从JDK1.5之后增加了泛型技术,此技术的核心意义在于:类属性或方法的参数在定义数据类型时,可以直接使用一个标记进行占位,在具体使用时才设置其对应的实际数据类型,这样当设置的数据类型出现错误后,就可以在程序编译时检测出来。
package com.yootk.demo; // 此时设置的T在Point类定义上只表示一个标记,在使用时需要为其设置具体的类型 class Point<T> { // 定义坐标,Type = 简写T,是一个类型标记 private T x ; // 此属性的类型不知道,由Point类使用时动态决定 private T y ; // 此属性的类型不知道,由Point类使用时动态决定 public Point(T x, T y) { this.x = x; this.y = y; } public T getX() { return x; } public T getY() { return y; } }
本程序在Point类声明时采用了泛型(class Point<T>)支持。
-
测试:
public class TestDemo { public static void main(String[] args) { // 第一步:根据需要设置数据,假设此时的作用是传递坐标 Point<String> p = new Point<String>("东经100度", "北纬20度") ; // 实例化Point类数据,设置泛型类型为String // 第二步:根据设置好的坐标取出数据进行操作 String x = p.getX(); // 取出坐标数据,不再需要强制转换 String y = p.getY(); // 取出坐标数据,不再需要强制转换 System.out.println("x坐标:" + x + ",y坐标:" + y); } }
本程序在实例化Point对象时使用了String作为泛型标记,这表示该Point对象在调用getter等方法时的返回值是String型的,从而避免了因参数设置错误导致的强制向下转型出现异常的情况。
-
注意:如果要采用了泛型定义类,那么在实例化的时候,所能采用的泛型标记只能是引用数据类型,不能是基本数据类型。基本数据类型可以采用其对应的包装类作为泛型标记。
-
如果在实例化泛型定义的类时,不设置泛型类型(即不设置泛型标记),则默认的泛型类型为Object类型
-
多个泛型标记,在开发中一个雷可以定义多个泛型声明:
class Point<P, R> { public R fun(P p) { return null ; } }
本程序设置了两个泛型标记,其中P表示方法参数类型标记,R表示方法返回值类型标记。
通配符
利用泛型技术虽然解决了向下转型所带来的安全隐患问题,但同时又会产生新的问题:即便是同一个类,由于泛型类型不同,其对象表示的含义也不同,因此不能直接进行引用操作。可以使用通配符 “?” 来解决这个问题。
package com.yootk.demo;
class Message<T> { // 类上使用泛型
private T msg;
public void setMsg(T msg) {
this.msg = msg;
}
public T getMsg() {
return msg;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<String> m1 = new Message<String>();
Message<Integer> m2 = new Message<Integer>();
m1.setMsg("www.yootk.com"); // 设置内容
m2.setMsg(30);
fun(m1); // 引用传递
fun(m2); // 程序错误,因为fun()方法参数的泛型类型是String
}
/**
* 接收Message类对象,并且调用getter方法输出类中的msg属性内容
* @param temp 接收Message类对象的引用传递,此处设置的泛型类型为String
*/
public static void fun(Message<String> temp) {
System.out.println(temp.getMsg());
}
}
fun()方法中接收的参数为泛型类型为String的Message对象,而m2为泛型类型为Integer的Message对象,所以没法正常进行引用传递,所以会报错。
-
使用通配符 “?” 来解决该问题。
public class TestDemo { public static void main(String[] args) { Message<Integer> m1 = new Message<Integer>(); Message<String> m2 = new Message<String>(); m1.setMsg(100); // 设置属性内容 m2.setMsg("www.yootk.com"); fun(m1); // 引用传递 fun(m2); } public static void fun(Message<?> temp) { // 不能设置,但是可以取出 System.out.println(temp.getMsg()); } }
这样一来,只要是Message类的对象,不管是什么泛型类型,fun()方法都可以接收。
-
问题:为什么不用Object作为泛型类型?
虽然Object类与String类为父子类关系,但Message<Object>和Message<String>则属于两个完全独立的概念。
如果在定义fun()方法时不设置泛型类型,确实可以实现任意泛型类型对象的接收,但也就产生了一个问题:如果不只拍任何具体泛型类型,则默认为Object类型,也就是说方法力可以随意修改属性内容,例如:
public class TestDemo { public static void main(String[] args) { Message<Integer> m1 = new Message<Integer>(); Message<String> m2 = new Message<String>(); m1.setMsg(100); m2.setMsg("www.yootk.com"); fun(m1); fun(m2); } public static void fun(Message temp) { // 随意修改属性内容,逻辑错误 temp.setMsg("魔乐科技软件学院:www.mldn.cn"); System.out.println(temp.getMsg()); } }
该程序在编译时不会报错,但会出现警告信息。
-
设置泛型上限和泛型下限:
-
方法(泛型类<? extends 类名> 对象名) {}; // 设置泛型上限,表示只接受泛型类型为该类或其子类的对象 public static void fun(Message<? extends Number> temp) {} // 设置泛型上限,表示该方法只接受泛型类型为Number类或其子类的Message对象
-
方法(泛型类<? super 类名> 对象名) {}; // 设置泛型下限,表示只接受泛型类型为该类的对象 public static void fun(Message<? super String> temp) {} // 定义泛型下限,表示该方法只接受泛型类型为String的对象
-
泛型接口
泛型不仅可以定义在类中,也可以定义在接口上。定义在接口的泛型被称为泛型接口。
-
定义泛型接口:
package com.yootk.demo; /** * 定义泛型接口,由于类与接口命名标准相同,为了区分出类与接口,在接口前加上字母“I”,例如:IMessage * 如果定义抽象类,则可以在前面加上Abstract,例如:AbstractMessage * @author YOOTK * @param <T> print()方法使用的泛型类型 */ interface IMessage<T> { // 定义泛型接口 public void print(T t); }
-
泛型接口的实现:
-
实现方式一:在子类继续设置泛型标记
class MessageImpl<S> implements IMessage<S> { // 在子类继续设置泛型,此泛型也作为接口中的泛型类型 public void print(S s) { System.out.println(s); } } public class TestDemo { public static void main(String[] args) { Message<String> msg = new MessageImpl<String>(); // 向上转型 msg.print("hello world"); } }
-
实现方式二:在子类不设置泛型,而为父接口明确定义一个泛型类型
class MessageImpl implements IMessage<String> { // 在子类不设置泛型,并设置具体泛型类型 public void print(String str) { System.out.println(str); } } public class TestDemo { public static void main(String[] args) { Message<String> msg = new MessageImpl(); // 向上转型 msg.print("hello world"); } }
-
泛型方法
泛型除了可以定义在类和接口上,还可以定义在方法上:
package com.yootk.demo;
public class TestDemo {
public static void main(String[] args) {
String str = fun("www.yootk.com"); // 泛型类型为String
System.out.println(str.length()); // 计算长度
}
/**
* 此方法为泛型方法,T的类型由传入的参数类型决定
* 必须在方法返回值类型前明确定义泛型标记
* @param t 参数类型,同时也决定了返回值类型
* @return 直接返回设置进来的内容
*/
public static <T> T fun(T t) { // 泛型方法定义
return t;
}
}
8.5 enum枚举
枚举是JDK1.5之后增加的一个主要新功能,利用枚举可以简化多例设计模式(一个类只能产生几个实例化对象)的定义。
同时在Java中的枚举也可以向普通类那样定义属性、构造方法、实现接口等。
认识枚举
利用enum关键字就可以定义枚举类型。
-
定义一个颜色的枚举类:
package com.yootk.demo; enum Color { // 定义枚举类 RED, GREEN, BLUE; // 表示此处为实例化对象 } public class TestDemo { public static void main(String[] args) { Color red = Color.RED; // 直接取得枚举对象 System.out.println(red); } } // 程序执行结果: RED
本程序定义了一个Color枚举类,同时在类中定义了3种颜色对象。
-
枚举只需要使用enum关键字就可以定义,但严格来说,枚举知识类结构的加强而已。因为在Java在中使用enum定义枚举类就相当于默认继承java.lang.Enum类,此类的定义如下:
public abstract class Enum<E extends Enum<E>> extends Object implements Comparable<E>, Serializable
-
从定义中可以发现,Enum类本身是一个抽象类,而抽象类在使用时必须被子类继承,同时在这个抽象类中提供了以下方法:
protected Enum(String name, int ordinal) // 构造,传递枚举对象的名字和序号 public final int ordinal() // 普通,取得当前枚举对象的序号 public final String name() // 普通,取得当前枚举对象的名字
-
可以看出Enum类的构造方法使用了protected权限定义,这也是一种封装。
~~~java package com.yootk.demo; enum Color { // 定义枚举类 RED, GREEN, BLUE; // 表示此处为实例化对象 } public class TestDemo { public static void main(String[] args) { Color red = Color.RED; // 直接取得枚举对象 System.out.println("枚举对象序号:" + red.ordinal()); System.out.println("枚举对象名称:" + red.name()); } } /* 程序执行结果: 枚举对象序号:0 枚举对象名称:RED */
本程序中,由于RED对象是最先被定义的,所以序号为0
-
enum和Enum的关系:
enum用来定义枚举类型,在Java中每一个使用enum定义的枚举类型实际上都表示一个类默认继承了Enum类。
-
枚举类除了继承了Enum抽象类提供的方法外,还定义了一个 values() 方法,这个方法会将枚举类中的所有对象以对象数组的形式返回:
package com.yootk.demo; enum Color { // 定义枚举类 RED, GREEN, BLUE; // 表示此处为实例化对象 } public class TestDemo { public static void main(String[] args) { for(Color c : Color.values()) { // 调用value()方法,返回值为枚举对象数组 System.out.println(c.ordinal() + " - " + c.name()); // 打印枚举对象的序号和名字 } } } /* 程序执行结果: 0-RED 1-GREEN 2-BLUE */
定义其他结构
-
既然枚举属于多例设计模式,那枚举中同样包含:属性、方法、构造方法。但有两点需要注意:
- 枚举中定义构造方法不能使用public声明,一定要进行封装,如果没有无参构造,要手动调用构造传递参数。
- 枚举对象必须放在首行,随后才可以定义属性、构造、普通方法等结构。
-
扩充枚举功能:
package com.yootk.demo; enum Color { RED("红色"), GREEN("绿色"), BLUE("蓝色"); // 实例化枚举对象,必须写在首行 private String title; // 属性 private Color(String title) { // 构造方法,不能使用public声明 this.title = title; } public String toString() { // 覆写toString()方法 return this.title; } } public class TestDemo { public static void main(String[] args) { for (Color c : Color.values()) { // 取得全部枚举对象 System.out.print(c + "、"); // 直接输出对象调用toString() } } } // 程序执行结果: 红色、绿色、蓝色
本程序在枚举中定义了有参构造方法,所以在首行进行实例化枚举对象时必须传递具体的参数内容,同时该类中又覆写了toString()方法,可以直接取得枚举对象的内容
-
枚举实现接口:
package com.yootk.demo; interface IMessage { public String getTitle() ; } enum Color implements IMessage { // 实现接口 RED("红色"), GREEN("绿色"), BLUE("蓝色"); // 定义枚举对象,都是IMessage接口实例 private String title; // 属性 private Color(String title) { // 构造方法,不能使用public声明 this.title = title; } public String getTitle() { // 覆写方法 return this.title ; } public String toString() { // 覆写toString()方法 return this.title; } } public class TestDemo { public static void main(String[] args) { IMessage msg = Color.RED ; // 实例化接口对象 System.out.println(msg.getTitle()); } }
由于枚举实现了接口,所以所有的枚举对象都是IMessage接口的实例
-
除了实现接口外,在枚举类中还可以直接定义抽象方法,这样枚举中的每一个对象都必须在声明时覆写抽象方法。
package com.yootk.demo; enum Color { RED("红色") { // 适应匿名内部类的方式实现接口 public String getTitle() { // 覆写getTitle() return this + " - red"; } }, GREEN("绿色") { // 适应匿名内部类的方式实现接口 public String getTitle() { return this + " - green"; } }, BLUE("蓝色") { // 适应匿名内部类的方式实现接口 public String getTitle() { return this + " - blue"; } }; private String title; // 属性 private Color(String title) { // 构造方法,不能使用public声明 this.title = title; } public String toString() { // 覆写toString()方法 return this.title; } public abstract String getTitle() ; // 定义抽象方法 } public class TestDemo { public static void main(String[] args) { System.out.println(Color.RED.getTitle()); } }
本程序在枚举类中定义了一个getTitle()的抽象方法,这样枚举中的每个对象都必须覆写此方法。
8.6 Annotation注解
利用Annotation注解可以回避面向对象中覆写方法名称固定的问题。
-
在Java SE中为了方便用户编写代码,提供了3种最为常见的基础Annotation定义,分别是:
- 准确的覆写:@Override
- 声明过期操作:@Deprecated
- 压制警告:@SuppressWarnings
-
关于软件开发问题:
为了更好地理解Annotation 的好处,下面设计一个简单的案例进行说明。假设有一套程序,由于某些操作需要连接3个不同的服务器(用户授权服务器、图片服务器、信息服务器),这3个服务器都有各自的地址,那么此时对于程序的实现就可能有以下3种方式。
- 方式一:将所有与配置相关的内容直接写到代码中;
- 优点:代码编写方便;
- 缺点:如果服务器地址变更或者相关信息增多,则代码维护困难。
- 方式二:将配置与程序代码独立,即程序运行时根据配置文件进行操作;
- 优点:代码维护方便,当信息变更时直接修改配置文件,而程序不需要改变;
- 缺点:当配置信息增多后,配置文件也会相应增加,程序维护困难
- 方式三:配置信息对用户而言无用,而且胡乱地修改还会导致程序错误,所以可以将配置信息写回到程序里,但是需要利用一些明显的标记来区分配置信息与程序。
- 优点:不再需要单独定义配置文件,可以减少代码数量;
- 缺点:需要容器支持,开发难度较高。
以上为读者列出的3种使用方式实际上也是软件开发的演变过程。在现在的开发中方式二与方式三使用最为广泛,虽然方式三的开发要比方式二更加容易,但是方式二适合于隐藏代码的模式(要变更直接修改配置文件,但是不需要提供给用户源代码)。至于开发中使用哪种,要根据开发者所在的项目团队来决定。
- 方式一:将所有与配置相关的内容直接写到代码中;
准确的覆写:@Override
在尽心覆写时,为了保证子类覆写的方法的确是父类定义过的方法,就可以加上 “ @Override ” 注解,这样即使用户覆写方法时出现错误,也可以在编译时直接检查出来。
class Book {
@Override // 只要正确进行了覆写,就不会出现编译的语法错误
public String toString() { // 原本打算覆写toString()
return "《名师讲坛 —— Oracle开发实战经典》" ;
}
}
本程序中Book类覆写了Object父类中的toString()方法,为了保证此方法覆写的正确性,在方法定义上使用了 “ @Override ” 注解。
-
不写 “ @Override ” 注解在正确覆写时没有任何问题,但是一旦覆写错误将无法验证。
class Book { public String tostring() { // 原本打算覆写toString() return "这是一本书!" ; } }
本来打算覆写toString()方法的,但字母大小写输入错误,导致此时无法覆写,如果此时有 “ @Override ” 注解,则会在程序编译时检查出来。
声明过期操作:@Deprecated
如果发现项目中的一个方法fun()功能不足,于是对于开发者有以下两个修改fun()方法的选择:
- 选择一:直接在新版本的工具包里面取消fun()方法,同时直接给出新的fun2()方法;
- 选择二:在新版本的开发包里面保存fun()方法,但是通过某种途径告诉新的开发者,此方法存在问题,并且提供fun20这个新的方法供开发者使用。
很明显,选择而会更加合适,因为第二种做法可以兼顾已使用项目的情况。这是就可以使用 “ @Deprecated ” 注解来声明过期的不建议使用的方法。
package com.yootk.demo;
class Book {
@Deprecated // 此方法为过期操作
public void fun() { // 使用会有警告,但是不会出错
}
}
public class TestDemo {
public static void main(String[] args) {
Book book = new Book();
book.fun(); // 此方法不建议使用
}
}
本程序中Book类中的fun()方法使用了 “ @Deprecated ” 注解声明,表示此方法不建议使用,如果在开发中继续使用,就会出现警告信息。
压制警告:@SuppressWarning
在使用了一些不安全的操作时,程序会在编译时出现一些安全警告,但如果开发者已经明确知道了这些但依然要这样使用,那么这些警告信息就会对开发者造成干扰。这时可以在有可能出现警告信息的代码上使用 ” @SuppressWarning “ 压制所有出现的警告信息。
package com.yootk.demo;
class Book<T> {
private T title;
public void setTitle(T title) {
this.title = title;
}
public T getTitle() {
return title;
}
}
public class TestDemo {
@SuppressWarnings({ "rawtypes", "unchecked" }) // 压制警告
public static void main(String[] args) {
Book book = new Book(); // 没有声明泛型,产生“rawtypes”警告信息
book.setTitle("HELLO"); // 出现警告信息,产生“unchecked”警告信息
}
}
8.7 接口定义强化
-
之前学习过接口的定义:只由抽象方法和全局常量组成。
-
但从JDK1.8开始,可以在接口中定义普通方法了(使用default声明)与静态方法(使用static声明)。
-
接口加强产生背景:假如某一个接口使用广泛,改接口已经产生了差不多30万个子类,但现在突然发现这个接口设计功能不足,需要扩充一些新的方法,并且这个方法对于所有子类实现是完全相同的,如果按以前的接口定义,父接口加一个抽象方法,其30万个子接口都要覆写这一相同的方法,很麻烦。如果接口可以定义普通方法的话,那么这个方法就可以被所有子类继承了。
-
定义普通方法必须要用default来定义,使用static来定义静态方法。
package com.yootk.demo; interface IMessage { // 定义接口 public void print(); // 这是一个接口里面原本定义的方法 default void fun1() { // 在接口里面定义了一个普通的方法 System.out.println("接口的普通方法"); } static void fun2() { // 在接口里面定义了一个静态的方法 System.out.println("接口的静态方法"); } } class MessageImpl implements IMessage { @Override public void print() { // 覆写print()方法 System.out.println("子类覆写接口方法"); } } public class TestDemo { public static void main(String[] args) { IMessage msg = new MessageImpl(); msg.print(); // 子类已覆写接口方法 msg.fun1(); // 接口的普通方法 Imessage.fun2(); // 接口的静态方法 } }
8.8 Lambda表达式
Lambda表达式是JDK1.8引入的重要技术特征。其主要应用于单一抽象方法接口环境下的一种简化定义形式,可以用于解决匿名内部类的定义复杂问题。
所以Lambda表达式只能应用于只有一个抽象方法的接口中。
-
Lambda表达式语法:
(参数) -> 方法体
-
匿名内部类:
package com.yootk.demo; interface IMessage { public void print() ; } public class TestDemo { public static void main(String[] args) { fun(new IMessage() { // 定义匿名内部类 @Override public void print() { System.out.println("匿名内部类") ; } }); } public static void fun(IMessage msg) { msg.print() ; } }
-
Lambda表达式解决:
package com.yootk.demo; interface IMessage { public void print() ; } public class TestDemo { public static void main(String[] args) { // 此处为Lamda表达式,没有任何输入参数,只是进行输出操作 fun(() -> System.out.println("Lambda表达式解决")); // (参数) -> 方法体 } public static void fun(IMessage msg) { msg.print() ; } }
可以看出Lambda表达式要比匿名内部类更加简洁
-
"@FunctionalInterface"注解:
Lambda明确只能在拥有一个抽象方法的接口上使用,但一个项目中往往有很多接口,为了分辨出哪些接口只有一个抽象方法,可以在接口上使用"@FunctionalInterface"注解声明,这样就表示此接口为函数式接口,里面只允许定义一个抽象方法。(8.9 方法引用,8.10 内建函数式接口)
@FunctionalInterface // "@FunctionalInterface"注解 interface IMessage { // 该接口为函数式接口,只能定义一个抽象方法 public void print() ; }
-
定义有参数有返回值的方法:
package com.yootk.demo; @FunctionalInterface interface IMessage { public int add(int x, int y); } public class TestDemo { public static void main(String[] args) { fun((s1, s2) -> { // 传递两个参数,此处只是一个参数标记 return s1 + s2; }); } public static void fun(IMessage msg) { System.out.println(msg.add(10, 20)); } }
如果只是简单计算并返回,则可以省略return,改为:
public static void main(String[] args) { fun((s1, s2) -> s1 + s2); // 直接返回两个参数的计算结果,省略return }
-
Lambda表达式与static方法配合:
package com.yootk.demo; @FunctionalInterface interface IMessage { public int add(int ... args); static int sum(int ... args) { // 此方法可以由接口名称直接调用 int sum = 0; for (int temp : args) { sum += temp; } return sum; } } public class TestDemo { public static void main(String[] args) { // 在Lamda表达式中直接调用接口里定义的静态方法 fun((int... param) -> IMessage.sum(param)); } public static void fun(IMessage msg) { System.out.println(msg.add(10, 20, 30)); // 传递可变参数 } }
8.9 方法引用
在Java中,对象的引用传递可以实现不同对象名称操作同一块内存空间。从JDK1.8开始,在方法上也支持了引用操作,这样就相当于为方法定义了别名。
-
对于方法的引用,Java8中一共定义了4种操作形式:
- 引用静态方法:类名称::方法名
- 引用某个对象的方法:对象名::方法名
- 引用特定类型的方法:特定类::方法名
- 引用构造方法: 类名称::new .
-
引用静态类,本次将举例引用String类中的静态方法valueOf()(public static String valueOf(int x))
package com.yootk.demo; @FunctionalInterface interface IMessage<P, R> { public R zhuanhuan(P p); } public class TestDemo { public static void main(String[] args) { // 将String.valueOf()方法变为IMessage接口里的zhuanhuan()方法 // valueOf()方法可以接收int型数据,返回String型数据 IMessage<Integer, String> msg = String::valueOf; String str = msg.zhuanhuan(1000); // 调用引用方法进行操作 System.out.println(str.replaceAll("0", "9")); } } // 程序执行结果: 1999
本程序将String类的valueOf()方法引用为Imessage接口中的zhuanhuan()方法,这样调用zhuanhuan()方法时,实际上相当于调用String.valueOf()方法。
-
引用普通方法,本次将引用String类中的toUpperCase()方法(public String toUpperCase())。
package com.yootk.demo; @FunctionalInterface interface IMessage<R> { public R upper() ; } public class TestDemo { public static void main(String[] args) { // String类的toUpperCase()定义:public String toUpperCase() // 此方法没有参数,但是有返回值,并且这个方法一定要在有实例化对象的情况下才可以调用 //“yootk”字符串是String类的实例化对象,所以可以直接调用toUpperCase()方法 // 将toUpperCase()函数的应用交给了IMessage接口 IMessage<String> msg = "yootk" :: toUpperCase ; String str = msg.upper() ; // 相当于“"yootk".toUpperCase()” System.out.println(str); } }
-
引用特定类的方法,正常情况下使用”类::方法“引用static方法,但有些形式的普通方法也可以这样引用。
如:String类中有一个方法:public int compareTo(String anotherString)
可以看到这种方法的代码形式是:String对象.compareTo(String对象) 的形式,也就是说如果要引用这个方法,就需要两个参数。
package com.yootk.demo; @FunctionalInterface interface IMessage<P> { public int compare(P p1,P p2) ; } public class TestDemo { public static void main(String[] args) { IMessage<String> msg = String :: compareTo ; // 引用String类的普通方法 // 传递调用的参数,形式为:"A".compareTo("B") System.out.println(msg.compare("A", "B")); } } // 程序执行结果: -1
由于String类中的compareTo()方法调用时需要指定对象,所以在使用其引用方法compare()时,就必须传递两个参数。
-
引用构造函数:
package com.yootk.demo; @FunctionalInterface interface IMessage<C> { public C create(String t, double p); // 引用构造方法 } class Book { private String title; private double price; public Book(String title, double price) { // 有两个参数的构造 this.title = title; this.price = price; } @Override public String toString() { return "书名:" + this.title + ",价格:" + this.price; } } public class TestDemo { public static void main(String[] args) { IMessage<Book> msg = Book::new; // 引用构造方法 // 调用的虽然是create(),但是这个方法引用的是Book类的构造 Book book = msg.create("Java开发实战经典", 79.8); System.out.println(book); } }
8.10 内建函数式接口
-
在方法引用的操作过程中,读者可以发现,不管如何进行操作,对于可能出现的函数式接口(使用"@FunctionalInterface"注解的接口)的方式也最多有4类:
- 有参数有返回值
- 有参数无返回值
- 无参数有返回值
- 判断真假
-
为了简化开发者的定义以及方法操作的同一,从JDK1.8开始提供一个新的开发包:java.util.function,里面提供了4个核心的函数式接口:
-
功能型接口(Function):此接口需要接收一个参数,并返回一个处理结果
@FunctionalInterface public interface Function<T, R> { public R apply(T t); }
-
消费型接口(Consumer):此接口负责接收数据(引用数据时不需要返回),并且不返回处理结果
@FunctionalInterface public interface Consumer<T> { public void accept(T t); }
-
供给型接口(Supplier):此接口不接收参数,但可以返回结果
@FunctionalInterface public interface Supplier<T> { public T get(); }
-
断言型接口(Predicate):进行判断操作
@FunctionalInterface public interface Predicate<T> { public boolean test(T t); }
-
-
以上的4个函数式接口除了指定的抽象方法外,还提供了一些default或static方法。
-
应用举例,使用Function接口引用String类的”publicboolean startsWith(String str)“方法:
package com.yootk.demo; import java.util.function.Function; public class TestDemo { public static void main(String[] args) { Function<String, Boolean> fun = "##yootk"::startsWith; System.out.println(fun.apply("##")); // 相当于利用对象调用startsWith() } }
本章小结
- Java新特性中提供了可变参数,这样在传递参数时就可以不用受到参数的个数限制,全部的参数将以数组的形式保存下来。
- foreach是 Java中的新特性,主要目的是方便地输出数或集合组中的内容。
- 泛型可以使程序的操作更加安全,避免发生类转换异常。
- 在程序中如果使用类时没有指定泛型,泛型将被擦除掉,将使用Object接收参数。
- 可以使用通配符 “?” 接收全部的泛型类型对象。
- 通过<? extends类>可以设置泛型的上限,通过<? super类>可以设置泛型的下限。
- 泛型方法可以定义在泛型类中,也可以定义在普通类中。
- 泛型也可以在接口中定义,实现泛型接口的子类可以指明具体的泛型类型也可以继续设置泛型标记
- 在程序中可以使用一个枚举来指定对象的取值范围,枚举就相当于一种简化版的多例设计模式。
- 在Java中使用enum 关键字定义一个枚举类,每一个枚举类都继承Enum类。
- 在枚举中可以通过values()方法取得枚举中的全部内容,并以对象数组的形式返回
- 在枚举类中可以定义构造方法,则在设置枚举范围时必须显式地调用构造方法。
- 一个枚举类可以实现一个接口或者直接定义一个抽象方法,但是每个枚举对象都必须分别实现全部的抽象方法。
- Annotation注解是 JDK1.5之后新增的功能,主要是使用注释的形式进行程序的开发。
- 在系统中提供了3个内建的Annotation注解:@Override、@Deprecated、@SuppressWarnings.
- Lambda表达式可以有效解决匿名内部类定义复杂的问题,使用更简单的函数式语句即可实现函数式接口(函数式接口可以使用“@FunctionalInterface”注解定义)。