Java8学习之路-Java8的发展
Java8虽然提出了很多新特性,但是在日常写项目和编程实践的使用还不是特别熟悉,写这个专栏记录一下Java8的学习之路。
JDK5
自动装、拆箱
从JDK5开始为所有基本数据类型都提供了与之对应的包装类,使基本数据类型也能够以OOP的方式来操作。
int -->Integer
double --> Double
long --> Long
char --> Character
float --> Float
boolean --> Boolean
short --> Short
byte -- > Byte
如下代码演示了基本数据类型和包装类的相互转换:
public class AutoBox {
public static void main(String[] args) {
int a = new Integer(66);
Integer b = 18;
Boolean flag = true;
boolean isBug = Boolean.FALSE;
}
}
自动装箱:将基本数据类型转换为对象:int
--> Integer
自动拆箱:将对象转换为基本数据类型:Integer
--> int
对于JDK1.5之前集合不能存储基本数据类型的问题,可使用自动装、拆箱来解决。
枚举
枚举是 JDK1.5 推出的一个比较重要的特性。其关键字为 enum
例如:定义代表交通灯的枚举。常用于代表某些状态值、标识符或一些特定的类型。
public enum MyEnum{
RED,GREEN,YELLOW
}
for-each遍历
常用于遍历集合,可以简化代码,逻辑更加清晰:
for(String s : strs){
System.out.println(s);
}
注意:使用for-each遍历集合时,要遍历的集合必须实现了Iterator接口
泛型
“泛型” 意味着编写的代码可以被不同类型的对象所重用。 可见泛型的提出是为了编写重用性更好的代码。 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
//给集合指定存入类型,上面这个集合在存入数据的时候必须存入String类型的数据,否则编译器会报错
List<String> strs = new ArrayList<String>();
静态导入
可以将类中的一些变量、方法以import static
的方式将其导入,使被导入类的静态变量和方法于当前类可见,使用无需再给出全类名。
优点:代码简洁;
缺点:过度使用会降低代码的可读性,若某个方法重名时,会带来歧义;
import static java.lang.System.out;
public class StaticImport {
public static void main(String[] args) {
out.println("Hi, Let's use the java 8!");
}
}
变长参数
在JDK1.5以前,当我们要为一个方法传递多个类型相同的参数时, 我们有两种方法解决
- 直接传递一个数组过去
- 有多少个参数就传递多少个参数。
例如:
public void printColor(String red,String green,String yellow){
// do something
}
或者:
public void printColor(String[] colors){
// do something
}
这样编写方法参数虽然能够实现我们想要的效果,但是,这样是不是有点麻烦呢? 再者,如果参数个数不确定,我们怎么办呢?Java JDK1.5为我们提供的可变参数就能够完美的解决这个问题.
例如:
public void printColor(String... colors){
// do something
}
如果参数的类型相同,那么可以使用 类型+三个点
,后面跟一个参数名称的形式。 这样的好处就是,只要参数类型相同,无论传递几个参数都没有限制 注意:可变参数必须是参数列表的最后一项(该特性对对象和基本数据类型都适用)。
线程并发库
线程并发库是 Java1.5 提出的关于多线程处理的高级功能,所在包:java.util.concurrent
包括
- 线程互斥工具类:Lock,ReadWriteLock
- 线程通信:Condition
- 线程池:ExecutorService
- 同步队列:ArrayBlockingQueue
- 同步集合:ConcurrentHashMap,CopyOnWriteArrayList
- 线程同步工具:Semaphore
JDK6
Compiler API
我们可以用JDK1.6 的Compiler API(JSR 199)去动态编译Java源文件, Compiler API结合反射功能就可以实现动态的产生Java代码并编译执行这些代码,有点动态语言的特征。
这个特性对于某些需要用到动态编译的应用程序相当有用,比如JSP Web Server,当我们手动修改JSP后, 是不希望需要重启Web Server才可以看到效果的,这时候我们就可以用Compiler API来实现动态编译JSP文件。 当然,现在的JSP Web Server也是支持JSP热部署的,现在的JSP Web Server通过在运行期间通过Runtime.exec或ProcessBuilder来调用javac来编译代码, 这种方式需要我们产生另一个进程去做编译工作,不够优雅而且容易使代码依赖与特定的操作系统; Compiler API通过一套易用的标准的API提供了更加丰富的方式去做动态编译,而且是跨平台的。
Console
JDK1.6 中提供了 java.io.Console
类专用来访问基于字符的控制台设备。 你的程序如果要与 Windows 下的 cmd 或者 Linux 下的 Terminal 交互,就可以用 Console
类代劳。 但我们不总是能得到可用的 Console,一个JVM是否有可用的 Console 依赖于底层平台和 JVM 如何被调用。 如果JVM是在交互式命令行(比如 Windows 的 cmd)中启动的,并且输入输出没有重定向到另外的地方,那么就可以得到一个可用的 Console 实例。
Desktop类和SystemTray类
前者可以用来打开系统默认浏览器浏览指定的URL,打开系统默认邮件客户端给指定的邮箱发邮件, 用默认应用程序打开或编辑文件(比如,用记事本打开以 txt 为后缀名的文件),用系统默认的打印机打印文档;
后者可以用来在系统托盘区创建一个托盘程序。
轻量级Http Server API
JDK1.6 提供了一个简单的 Http Server API,据此我们可以构建自己的嵌入式 Http Server, 它支持Http和Https协议,提供了HTTP1.1的部分实现,没有被实现的那部分可以通过扩展已有的 Http Server API来实现, 程序员必须自己实现 HttpHandler 接口,HttpServer 会调用 HttpHandler
实现类的回调方法来处理客户端请求, 在这里,我们把一个 Http 请求和它的响应称为一个交换,包装成 HttpExchange
类,HttpServer
负责将 HttpExchange
传给 HttpHandler
实现类的回调方法。
对脚本语言的支持
如:ruby,groovy,javascript。
下面展示了如何在Java中调用js代码。
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("ECMAScript");
JDK7
捕获多异常
public static void first(){
try {
BufferedReader reader = new BufferedReader(new FileReader(""));
Connection con = null;
Statement stmt = con.createStatement();
} catch (IOException | SQLException e) {
//捕获多个异常,e就是final类型的
e.printStackTrace();
}
}
优点:用一个 catch
处理多个异常,比用多个 catch
每个处理一个异常生成的字节码要更小更高效。
数字变量对下滑线的支持
JDK1.7可以在数值类型的变量里添加下滑线,但是有几个地方是不能添加的
- 数字的开头和结尾
- 小数点前后
- F或者L前
int num = 1234_5678_9;
float num2 = 222_33F;
long num3 = 123_000_111L;
比如我们需要让线程休眠10s,如果我们直接写10000语义上不太清晰,而如果换成10_000
就比较清晰的可以表示休眠10s。
switch对String的支持
String status = "orderState";
switch (status) {
case "ordercancel":
System.out.println("订单取消");
break;
case "orderSuccess":
System.out.println("预订成功");
break;
default:
System.out.println("状态未知");
}
try-with-resource
try-with-resources
是一个定义了一个或多个资源的 try 声明,这个资源是指程序处理完它之后需要关闭它的对象。try-with-resources
确保每一个资源在处理完成后都会被关闭。
可以使用try-with-resources的资源有:任何实现了 java.lang.AutoCloseable
接口 java.io.Closeable
接口的对象。
例如:
public static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在 java 7 以及以后的版本里,BufferedReader
实现了 java.lang.AutoCloseable
接口。 由于 BufferedReader
定义在 try-with-resources
声明里,无论 try
语句正常还是异常的结束, 它都会自动的关掉。而在 java7 以前,你需要使用 finally
块来关掉这个对象。
创建泛型时类型推断
只要编译器可以从上下文中推断出类型参数,你就可以用一对空着的尖括号 <>
来代替泛型参数。 这对括号私下被称为菱形(diamond)。 在Java SE 7之前,你声明泛型对象时要这样
List<String> list = new ArrayList<String>();
而在Java SE7以后,你可以这样
List<String> list = new ArrayList<>();
因为编译器可以从前面(List)推断出推断出类型参数,所以后面的 ArrayList
之后可以不用写泛型参数了,只用一对空着的尖括号就行。 当然,你必须带着菱形 <>
,否则会有警告的。 Java SE7 只支持有限的类型推断:只有构造器的参数化类型在上下文中被显著的声明了,你才可以使用类型推断,否则不行。
List<String> list = new ArrayList<>();l
list.add("A");
//这个不行
list.addAll(new ArrayList<>());
// 这个可以
List<? extends String> list2 = new ArrayList<>();
list.addAll(list2);
Java8
Base64
对 Base64 编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码,例子代码如下:
final String text = "Lets Learn Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString(text.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded);
final String decoded = new String(
Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8);
System.out.println(decoded);
新的Base64API也支持URL和MINE的编码解码。
新的日期时间 API
Java 8引入了新的Date-Time API(JSR 310)来改进时间、日期的处理。时间和日期的管理一直是最令Java开发者痛苦的问题。 java.util.Date 和后来的 java.util.Calendar 一直没有解决这个问题(甚至令开发者更加迷茫)。
因为上面这些原因,诞生了第三方库Joda-Time,可以替代Java的时间管理API。 Java 8中新的时间和日期管理API深受Joda-Time影响,并吸收了很多Joda-Time的精华。 新的java.time
包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。 新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。
第二,关注下LocalDate和LocalTime类。LocalDate仅仅包含ISO-8601日历系统中的日期部分;LocalTime则仅仅包含该日历系统中的时间部分。这两个类的对象都可以使用Clock对象构建得到。 LocalDateTime类包含了LocalDate和LocalTime的信息,但是不包含ISO-8601日历系统中的时区信息。这里有一些关于LocalDate和LocalTime的例子: 如果你需要特定时区的data/time信息,则可以使用ZoneDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。
lambda表达式
Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法, 或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。很多JVM平台上的语言(Groovy、Scala等)从诞生之日就支持Lambda表达式,但是Java开发者没有选择,只能使用匿名内部类代替Lambda表达式。 Lambda的设计耗费了很多时间和很大的社区力量,最终找到一种折中的实现方案,可以实现简洁而紧凑的语言结构。最简单的Lambda表达式可由逗号分隔的参数列表、->符号和语句块组成。
Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了)。
public class Lambda {
public static void main(String[] args) {
Arrays.asList("a", "b", "d").forEach(System.out::println);
}
}
函数式接口
lambda表达式的设计者为了让lambda表达式和现有的接口有更好配合,提供了一个新的注解FunctionalInterface
用来标注这是一个函数式接口。会使编译器在编译器检测接口是否只有一个抽象方法配合lambda表达式使用。
Optional
Optional
Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了 Optionals
类来解决 NullPointerException
, 从而避免源码被各种 null
检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。 Optional
仅仅是一个容易存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查,可以参考Java 8官方文档了解更多细节。
如果Optional实例持有一个非空值,则 isPresent()
方法返回true,否则返回false;orElseGet()
方法,Optional实例持有null, 则可以接受一个lambda表达式生成的默认值;map()方法可以将现有的 Optional
实例的值转换成新的值;orElse()方法与orElseGet()方法类似, 但是在持有null的时候返回传入的默认值。
Streams
新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。 这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码。
Task 类有一个分数(或伪复杂度)的概念,另外还有两种状态:OPEN 或者 CLOSED。现在假设有一个task集合, 首先看一个问题:在这个task集合中一共有多少个OPEN状态的点?在Java 8之前,要解决这个问题,则需要使用foreach循环遍历task集合; 但是在Java 8中可以利用steams解决:包括一系列元素的列表,并且支持顺序和并行处理。
final Collection<Task> tasks = Arrays.asList(
new Task(Status.OPEN, 5),
new Task(Status.OPEN, 13),
new Task(Status.CLOSED, 8)
);
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter(task -> task.getStatus() == Status.OPEN)
.mapToInt(Task::getPoints)
.sum();
System.out.println("Total points: " + totalPointsOfOpenTasks);
这里有很多知识点值得说。首先,tasks集合被转换成steam表示;其次,在steam上的filter操作会过滤掉所有CLOSED的task; 第三,mapToInt操作基于每个task实例的Task::getPoints方法将task流转换成Integer集合;最后,通过sum方法计算总和,得出最后的结果。
更好的类型推断
Java 8 编译器在类型推断方面有很大的提升,在很多场景下编译器可以推导出某个参数的数据类型,从而使得代码更为简洁。
参数 Value.defaultValue()
的类型由编译器推导得出,不需要显式指明。在Java 7中这段代码会有编译错误,除非使用 Value.<String>defaultValue()
。
并行数组
Arrays.parallelSort
可以在多核情况下显著提高对数组排序的效率。
Nashron引擎
提供了nashron引擎可以在Java代码中直接编写js代码执行。
接口的默认方法和静态方法
Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法。
默认方法使得接口有点类似traits,不过要实现的目标不一样。默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。 默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写 由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。
尽管默认方法有这么多好处,但在实际开发中应该谨慎使用:在复杂的继承体系中,默认方法可能引起歧义和编译错误。