本篇博客你將學到:
1.Lambda表達式
2.Optional類,告別空指針異常
3.Stream流式處理
4.時間處理LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Clock、Duration
5.重復注解
6.擴展注釋
7.更好的類型推薦機制
8.參數名字保存在字節碼中
9.異步調用 CompletableFuture
1. Lambda表達式
? ? ? ?在JDK8之前,一個方法能接受的參數都是變量,例如: object.method(Object o)
,那么,如果需要傳入一個動作呢?比如回調。那么你可能會想到匿名內部類。例如:
? ? ? ?首先定義一個業務類:
public class Person {
public void create(String name, PersonCallback personCallback)
{
System.out.println("執行主業務方法");
personCallback.callback();
}
}
? ? ? ?匿名內部類是需要依賴接口的,所以需要定義個接口:
@FunctionalInterface
public interface PersonCallback {
void callback();
}
? ? ? ?編寫測試類:
public class Test {
public static void main(String[] args) {
new Person().create("源碼之路", new PersonCallback() {
@Override
public void callback() {
System.out.println("調用回調函數");
}
});
}
}
打印結果:
? ? ? ?上面的PersonCallback其實就是一種動作,但是我們真正關心的只有callback方法里的內容而已,為了寫callback里面的內容,我還要生成一個內部類并實現callback方法。在java8以前,這也是沒辦法的事情,因為一個方法傳入的參數必須是java原生變量和對象,不能傳遞方法。java8改變了增加一個一種參數傳遞方式,那就是我們可以傳遞一個方法了,即Lambda表達式。我們對上述代碼做如下修改:
public class Test {
public static void main(String[] args) {
new Person().create("源碼之路",()->{
System.out.println("調用回調函數");
});
}
}
? ? ? ?()->{ System.out.println("調用回調函數"); }
就是Lambda表達式啦,他是接口PersonCallback 的實現者,Lambda允許把函數作為一個方法的參數,一個lambda由用逗號分隔的參數列表、 –>符號、函數體符號、函數體三部分表示。對于上面的表達式,()為Lambda表達式的入參,這里為空,{ System.out.println("調用回調函數"); }為函數體,重點是這個表達式是沒有名字的。
? ? ? ?我們知道,當我們實現一個接口的時候,肯定要實現接口里面的方法,那么現在一個Lambda表達式應該也要遵循這一個基本準則,那么一個Lambda表達式它實現了接口里的什么方法呢?
? ? ? ?答案是:一個Lambda表達式實現了接口里的有且僅有的唯一一個抽象方法
。那么對于這種接口就叫做函數式接口。再次強調,這個接口必須只能有且只有一個抽象方法。Lambda表達式其實完成了實現接口并且實現接口里的方法這一功能,也可以認為Lambda表達式代表一種動作。我們可以直接把這種特殊的動作進行傳遞。
? ? ? ?我們現在在接口方法里傳遞一個參數:
@FunctionalInterface
public interface PersonCallback {
void callback(String name);
}
public class Test {
public static void main(String[] args) {
new Person().create("源碼之路",(name)->{
System.out.println("調用回調函數:"+name);
});
}
}
由此可見,()就是用來傳遞參數列表的。對于上面的表達式我們還可以進行簡化:
public class Test {
public static void main(String[] args) {
new Person().create("源碼之路", name->System.out.println("調用回調函數:"+name) );
}
}
? ? ? ?小括號()去掉了,這歸功于Java8的類型推導機制。因為現在接口里只有一個方法,那么現在這個Lambda表達式肯定是對應實現了這個方法,既然是唯一的對應關系,那么入參肯定是String類型,所以可以簡寫,并且方法體只有唯一的一條語句,所以也可以簡寫,以達到表達式簡潔的效果。也就是說java8能自動匹配你的參數和方法體中的內容,當然如果你的方法體有很多條語句的話,{}還是不能省略的。
是方法也是實例
考慮這么一種情況,PersonCallback 是一個接口,我要生成一個該接口的實現類的對象,傳統做法是編寫一個類實現這個接口,然后實現這個接口的方法,如:
public class PersonCallBackImpl implements PersonCallback {
@Override
public void callback(String name) {
System.out.println(name);
}
}
然后
PersonCallback per = new PersonCallBackImpl ();
per.callback("源碼之路:");
? ? ? ?就算是java8,工作中我們還是會這么用,因為上文說了,Lambda表達式要求你的接口只能有一個方法,而我們的業務接口往往有多個方法,不適合Lambda表達式。但是,如果我的接口就是一個方法,而我就是要實現一個實例且調用接口方法,上面寫法很麻煩,換種寫法看看:
PersonCallback personCallback = (name) ->{System.out.println(name);};
personCallback.callback("源碼之路");
? ? ? ?(name) ->{System.out.println(name);}
就是表示接口類型的對象。
? ? ? ?有的讀者包括我同事會說,我一直用實現類的方式,Lambda我不習慣,而且感覺可讀性差,我不想學,我現在的夠用了!OK,沒人逼著你學新東西,但是作為我們IT人,就是要不斷的學習不斷地更新自己的技術棧,不然就要被淘汰,沒辦法,技術更新換代太快了。中國有句俗語,熟能生巧,IT也是,再難的技術,多研究幾遍,也就那么回事。
? ? ? ?()里就一個參數,如果我們加一個,或者不加會怎樣:
? ? ? ?報錯了,其實上文講過了,這是源于java8的類型推導機制,接口中唯一的方法只有一個參數,編譯器很智能的推導出來了,而且一個參數的話可以省略小括號,無參或者多參都不能省略小括號。此外,不但能推導出方法參數的個數,還能推導出參數的類型。
看見沒,編譯器能推導出參數類型為String,如果傳一個int型參數,就會報錯。
1.1 函數式接口
一個接口里面只有一個抽象方法,這個接口就是函數式接口,這其實上面說的PersonCallback 就是函數式接口,這只是一種定義,不過函數式接口有個標志,就是@FunctionalInterface修飾了這個接口。
@FunctionalInterface
public interface PersonCallback {
void callback(String name);
}
這個注解起到了校驗的作用,比如,我在上面的接口中再增加一個抽象方法就會編譯錯誤:
有讀者說了,要你這么說,我連PersonCallback也不想寫,我每次都要定義一個接口才能使用Lambda表達式,比如我就想傳遞兩個int類型數值a和b,然后計算a+b的值返回,那我該怎么做?
首先,定義一個函數式接口:
@FunctionalInterface
public interface Caculate {
int add(int a , int b);
}
使用,
Caculate caculate = (a,b)->{return a+b;};
caculate.add(1,2);
其實,JDK8中也增加了很多函數式接口,比如java.util.function包,比如這四個常用的接口:
Supplier 無參數,返回一個結果
Function 接受一個輸入參數,返回一個結果
Consumer 接受一個輸入參數,無返回結果
Predicate 接受一個輸入參數,返回一個布爾值結果
我們可以直接使用,不需要自己去定義函數式接口,減少代碼量:
Supplier:
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public class Test {
public static void main(String[] args) {
Supplier<String> supplier = ()->{return "源碼之路";};
String retu = supplier.get();
}
}
Function :
public class Test {
public static void main(String[] args) {
Function<Integer, String> function = (a)->{return a+b;};
String retu = function.apply(3);
}
}
Consumer
public class Test {
public static void main(String[] args) {
Consumer<String> consumer = (a)->{System.out.println(a);};
consumer.accept("源碼之路");
}
}
public class Test {
public static void main(String[] args) {
Predicate<String> predicate = (a)->{
if(a.equals("源碼之路")){
return true;}
else{return false;}
};
boolean isSuccess = predicate.test("源碼之路");
}
}
? ? ? ?Java8中提供給我們這么多函數式接口就是為了讓我們寫 中提供給我們這么多函數式接口就是為了讓我們寫Lambda表達式更加方便 表達式更加方便,當然遇到特殊情況,你還是需要定義你自己的函數式接口然后才能寫對應的Lambda表達式。
Lambda表達式還有個技巧,就是如果你實現的接口需要返回數據,且你實現的方法體只有一行語句,則可以省略return關鍵字,大家自己調試,學技術要自己動手嘗試。
假如一個接口有三個實現類:
public interface PersonInterface {
void getName();
}
public class YellowPerson implements PersonInterface {
@Override
public void getName() {
System.out.println("yellow");
}
}
public class WhitePerson implements PersonInterface {
@Override
public void getName() {
System.out.println("white");
}
}
public class BlackPerson implements PersonInterface {
@Override
public void getName() {
System.out.println("black");
}
}
? ? ? ?現在我需要在PersonInterface接口中新增一個方法,那么勢必它的三個實現類都需要做相應改動才能編譯通過,這里我就不進行演示了,那么我們在最開始設計的時候,其實可以增加一個抽象類PersonAbstract,三個實現類改為繼承這個抽象類,按照這種設計方法,對PersonInterface接口中新增一個方法是,其實只需要改動PersonAbstract類去實現新增的方法就好了,其他實現類不需要改動了:
public interface PersonInterface {
void getName();
void walk();
}
public abstract class PersonAbstract implements PersonInterface {
@Override
public void walk() {
System.out.println("walk");
}
}
public class BlackPerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("black");
}
}
public class WhitePerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("white");
}
}
public class YellowPerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("yellow");
}
}
? ? ? ?在Java8中支持直接在接口中添加已經實現了的方法,一種是Default方法(默認方法),一種Static方法(靜態方法)。
1.2 接口的默認方法
? ? ? ?在接口中用default修飾的方法稱為默認方法 默認方法。接口中的默認方法一定要有默認實現(方法體),接口實現者可以繼承它,也可以覆蓋它。
default void testDefault(){
System.out.println("default");
};
public class Test {
public static void main(String[] args) {
PersonCallback personCallback = (name)->{System.out.println(name);};
personCallback.testDefault();
}
}
1.3 靜態方法
? ? ? ?在接口中用static修飾的方法稱為靜態方法 靜態方法。
static void testStatic(){
System.out.println("static");
};
調用方式:
TestInterface.testStatic();
? ? ? ?因為有了默認方法和靜態方法,所以你不用去修改它的實現類了,可以進行直接調用。注意,默認方法和靜態方法可以寫多個,實現類只能調用默認方法不能調用靜態方法,靜態方法只能通過TestInterface.testStatic()的方式調用。
? ? ? ?這也正對應了“迪米特法則”面向接口編程!
1.4 方法引用
? ? ? ?有個函數式接口Consumer,里面有個抽象方法accept能夠接收一個參數但是沒有返回值,這個時候我想實現accept方法,讓它的功能為打印接收到的那個參數,那么我可以使用Lambda表達式這么做:
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("?源碼之路");
? ? ? ?但是其實我想要的這個功能PrintStream類(也就是System.out的類型)的println方法已經實現了,這一步還可以再簡單點,如:
Consumer<String> consumer = System.out::println;
consumer.accept("?源碼之路");
打印結果:
? ? ? ?這就是方法引用,方法引用方法的參數列表必須與函數式接口的抽象方法的參數列表保持一致,返回值不作要求。
引用方法
- 實例對象::實例方法名
- 類名::靜態方法名
- 類名::實例方法名
實例對象::實例方法名
Consumer<String> consumer = System.out::println;
consumer.accept("????");
System.out代表的就是PrintStream類型的一個實例, println是這個實例的一個方法。
類名::靜態方法名
Function<Long, Long> f = Math::abs;
Long result = f.apply(-3L);
? ? ? ?Math是一個類而abs為該類的靜態方法。 Function中的唯一抽象方法apply方法參數列表與abs方法的參數列表相同,都是接收一個Long類型參數。
類名::實例方法名
? ? ? ?若Lambda表達式的參數列表的第一個參數,是實例方法的調用者,第二個參數(或無參)是實例方法的參數時,就可以使用這種方法
BiPredicate<String, String> b = String::equals;
b.test("a", "b");
? ? ? ?String是一個類而equals為該類的定義的實例方法。 BiPredicate中的唯一抽象方法test方法參數列表與equals方法的參數列表相同,都是接收兩個String類型參數。
引用構造器
? ? ? ?在引用構造器的時候,構造器參數列表要與接口中抽象方法的參數列表一致,格式為 類名::new。如:
Function<Integer, StringBuffer> fun = StringBuffer::new;
StringBuffer buffer = fun.apply(10);
? ? ? ?Function接口的apply方法接收一個參數,并且有返回值。在這里接收的參數是Integer類型,與StringBuffer類的一個構造方法StringBuffer(int capacity)對應,而返回值就是StringBuffer類型。上面這段代碼的功能就是創建一個Function實例,并把它apply方法實現為創建一個指定初始大小的StringBuffer對象。
引用數組
? ? ? ?引用數組和引用構造器很像,格式為 類型[]::new,其中類型可以為基本類型也可以是類。如:
Function<Integer, int[]> fun = int[]::new;
int[] arr = fun.apply(10);
Function<Integer, Integer[]> fun2 = Integer[]::new;
Integer[] arr2 = fun2.apply(10);
2.Optional
? ? ? ?空指針異常是導致Java應用程序失敗的最常見原因,以前,為了解決空指針異常, Google公司著名的Guava項目引入了Optional類, Guava通過使用檢查空值的方式來防止代碼污染,它鼓勵程序員寫更干凈的代碼。受到Google Guava的啟發, Optional類已經成為Java 8類庫的一部分。
? ? ? ?Optional實際上是個容器:它可以保存類型T的值,或者僅僅保存null。 Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。
我們先看Optional介紹,再來實戰。
創建Optional對象的幾個方法:
1. Optional.of(T value), 返回一個Optional對象, value不能為空,否則會出空指針異常
2. Optional.ofNullable(T value), 返回一個Optional對象, value可以為空
3. Optional.empty(),代表空
其他API:
1. optional.isPresent(),是否存在值(不為空)
2. optional.ifPresent(Consumer<? super T> consumer), 如果存在值則執行consumer
3. optional.get(),獲取value
4. optional.orElse(T other),如果沒值則返回other
5. optional.orElseGet(Supplier<? extends T> other),如果沒值則執行other并返回
6. optional.orElseThrow(Supplier<? extends X> exceptionSupplier),如果沒值則執行exceptionSupplier, 并拋出異常
那么,我們之前對于防止空指針會這么寫:
public class Order {
String name;
public String getOrderName(Order order ) {
if (order == null) {
return null;
}
return order.name;
}
}
現在用Optional,會改成:
public class Order {
String name = "源碼之路";
public String getOrderName(Order order ) {
// if (order == null) {
// return null;
// }
//
// return order.name;
Optional<Order> orderOptional = Optional.ofNullable(order);
if (!orderOptional.isPresent()) {
return null;
}
return orderOptional.get().name;
}
}
? ? ? ?那么如果只是改成這樣,實質上并沒有什么分別,事實上isPresent() 與 obj != null 無任何分別,并且在使用get()之前最好都使用isPresent() ,比如下面的代碼在IDEA中會有警告:'Optional.get()' without 'isPresent()' check。另外把 Optional 類型用作屬性或是方法參數在 IntelliJ IDEA 中更是強力不推薦的。
? ? ? ?對于上面的代碼我們利用IDEA的提示可以優化成一行( 666!):
public class Order {
String name;
public String getOrderName(Order order ) {
// if (order == null) {
// return null;
// }
// return order.name;
return Optional.ofNullable(order).map(order1 -> order1.name).orElse(null);
}
}
這個優化過程中map()起了很大作用。
第一次用,感覺很抽象,難以理解,也不習慣這么用。我們看以下Optional的源碼,大家就會理解很多:
public final class Optional<T> {
//EMPTY常量,即存放空值的Optional對象
private static final Optional<?> EMPTY = new Optional();
//被存放的值,可為null或非null值
private final T value;
//私有構造方法,創建一個包含空值的Optional對象
private Optional() {
this.value = null;
}
//私有構造方法,創建一個非空值的Optional對象
private Optional(T var1) {
this.value = Objects.requireNonNull(var1);
}
//這個方法很簡單,作用是返回一個Optional實例,里面存放的value是null
public static <T> Optional<T> empty() {
Optional var0 = EMPTY;
return var0;
}
//很簡單,就是返回一個包含非空值的Optional對象
public static <T> Optional<T> of(T var0) {
return new Optional(var0);
}
// 很簡單,返回一個可以包含空值的Optional對象
public static <T> Optional<T> ofNullable(T var0) {
return var0 == null ? empty() : of(var0);
}
//得到Optional對象里的值,如果值為null,則拋出NoSuchElementException異常
public T get() {
if (this.value == null) {
throw new NoSuchElementException("No value present");
} else {
return this.value;
}
}
//很簡單,判斷值是否不為null
public boolean isPresent() {
return this.value != null;
}
// 當值不為null時,執行consumer
public void ifPresent(Consumer<? super T> var1) {
if (this.value != null) {
var1.accept(this.value);
}
}
/*
*看方法名就知道,該方法是過濾方法,過濾符合條件的Optional對象,這里的條件用Lambda表達式來定義,
*如果入參predicate對象為null將拋NullPointerException異常,
*如果Optional對象的值為null,將直接返回該Optional對象,
*如果Optional對象的值符合限定條件(Lambda表達式來定義),返回該值,否則返回空的Optional對象
*/
public Optional<T> filter(Predicate<? super T> var1) {
Objects.requireNonNull(var1);
if (!this.isPresent()) {
return this;
} else {
return var1.test(this.value) ? this : empty();
}
}
/**
* 前面的filter方法主要用于過濾,一般不會修改Optional里面的值,map方法則一般用于修改該值,并返回修改后的Optional對象
* 如果入參mapper對象為null將拋NullPointerException異常,
* 如果Optional對象的值為null,將直接返回該Optional對象,
* 最后,執行傳入的lambda表達式,并返回經lambda表達式操作后的Optional對象
*/
public <U> Optional<U> map(Function<? super T, ? extends U> var1) {
Objects.requireNonNull(var1);
return !this.isPresent() ? empty() : ofNullable(var1.apply(this.value));
}
/**
* flatMap方法與map方法基本一致,唯一的區別是,
* 如果使用flatMap方法,需要自己在Lambda表達式里將返回值轉換成Optional對象,
* 而使用map方法則不需要這個步驟,因為map方法的源碼里已經調用了Optional.ofNullable方法;
*/
public <U> Optional<U> flatMap(Function<? super T, Optional<U>> var1) {
Objects.requireNonNull(var1);
return !this.isPresent() ? empty() : (Optional)Objects.requireNonNull(var1.apply(this.value));
}
//很簡單,當值為null時返回傳入的值,否則返回原值;
public T orElse(T var1) {
return this.value != null ? this.value : var1;
}
//功能與orElse(T other)類似,不過該方法可選值的獲取不是通過參數直接獲取,而是通過調用傳入的Lambda表達式獲取
public T orElseGet(Supplier<? extends T> var1) {
return this.value != null ? this.value : var1.get();
}
//當遇到值為null時,根據傳入的Lambda表達式跑出指定異常
public <X extends Throwable> T orElseThrow(Supplier<? extends X> var1) throws Throwable {
if (this.value != null) {
return this.value;
} else {
throw (Throwable)var1.get();
}
}
public boolean equals(Object var1) {
if (this == var1) {
return true;
} else if (!(var1 instanceof Optional)) {
return false;
} else {
Optional var2 = (Optional)var1;
return Objects.equals(this.value, var2.value);
}
}
public int hashCode() {
return Objects.hashCode(this.value);
}
public String toString() {
return this.value != null ? String.format("Optional[%s]", this.value) : "Optional.empty";
}
}
使用案例
閱讀完源碼(很簡單有沒有),我們舉幾個使用例子來加深印象:
ifPresent
,當值不為null時,執行Lambda表達式
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcDef");
//值不為null,執行Lambda表達式,
test.ifPresent(name -> {
String s = name.toUpperCase();
System.out.println(s);
});
//打印ABCDEF
}
}
filter
,如果Optional對象的值符合限定條件(Lambda表達式來定義),返回該值,否則返回空的Optional對象
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//過濾值的長度小于3的Optional對象
Optional<String> less3 = test.filter((value) -> value.length() < 3);
//打印結果
System.out.println(less3.orElse("不符合條件,不打印值!"));
}
}
map
,如果Optional對象的值為null,將直接返回該Optional對象,否則,執行傳入的lambda表達式,并返回經lambda表達式操作后的Optional對象
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//將值修改為大寫
Optional<String> less3 = test.map((value) -> value.toUpperCase());
//打印結果 ABCD
System.out.println(less3.orElse("值為null,不打印!"));
}
}
orElseGet
,功能與orElse(T other)類似,不過該方法可選值的獲取不是通過參數直接獲取,而是通過調用傳入的Lambda表達式獲取
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable(null);
System.out.println(test.orElseGet(() -> "hello"));
//將打印hello
}
}
orElseThrow
,當遇到值為null時,根據傳入的Lambda表達式跑出指定異常
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable(null);
//這里的Lambda表達式為構造方法引用
System.out.println(test.orElseThrow(NullPointerException::new));
//將打印hello
}
}
Optional總結:
? ? ? ?使用 Optional 時盡量不直接調用 Optional.get() 方法, Optional.isPresent() 更應該被視為一個私有方法, 應依賴于其他像 Optional.orElse(), Optional.orElseGet(), Optional.map() 等這樣的方法。
3. Stream
? ? ? ?Java8中的Stream是對集合(Collection)對象功能的增強,它專注于對集合對象進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量數據操作 (bulk data operation)。 Stream API 借助于同樣新出現的Lambda表達式,極大的提高編程效率和程序可讀性。同時它提供串行和并行兩種模式進行匯聚操作,并發模式能夠充分利用多核處理器的優勢,使用 fork/join 并行方式來拆分任務和加速處理過程。通常編寫并行代碼很難而且容易出錯, 但使用 Stream API 無需編寫一行多線程的代碼,就可以很方便地寫出高性能的并發程序。
? ? ? ?所以說,Java8中首次出現的java.util.stream是一個函數式語言+多核時代綜合影響的產物。
? ? ? ?但在當今這個數據大爆炸的時代,在數據來源多樣化、數據海量化的今天,很多時候不得不脫離 RDBMS,或者以底層返回的數據為基礎進行更上層的數據統計。而 Java 的集合 API 中,僅僅有極少量的輔助型方法,更多的時候是程序員需要用 Iterator 來遍歷集合,完成相關的聚合應用邏輯。這是一種遠不夠高效、笨拙的方法。在Java7中,如果要找一年級的所有學生,然后返回按學生分數值降序排序好的學生ID的集合,我們需要這樣寫:
package stream;
//學生類
public class Student {
private Integer id;
private Grade grade;
private Integer score;
public Student(Integer id, Grade grade, Integer score) {
this.id = id;
this.grade = grade;
this.score = score;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
}
//班級類
package stream;
public enum Grade {
FIRST, SECOND, THTREE
}
package stream;
import com.google.common.collect.Lists;
import java.util.*;
public class Test {
public static void main(String[] args) {
final Collection<Student> students = Arrays.asList(
new Student(1, Grade.FIRST, 60),
new Student(2, Grade.SECOND, 80),
new Student(3, Grade.FIRST, 100),
new Student(4, Grade.FIRST, 78),
new Student(5, Grade.FIRST, 92)
);
List<Student> gradeOneStudents = Lists.newArrayList();
for (Student student: students) {
if (Grade.FIRST.equals(student.getGrade())) {
gradeOneStudents.add(student);
}
}
Collections.sort(gradeOneStudents, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.getScore().compareTo(o1.getScore());
}
});
List<Integer> studentIds = new ArrayList<>();
for(Student t: gradeOneStudents){
studentIds.add(t.getId());
System.out.println("id:"+t.getId()+"——得分"+t.getScore());
}
}
}
打印結果:
? ? ? ?而在 Java 8 使用 Stream,代碼更加簡潔易讀;而且使用并發模式,程序執行速度更快,只需要將stram()變成parallelStream()即可。
public static void main(String[] args) {
final Collection< Student > students= Arrays.asList(
new Student(1, Grade.FIRST, 60),
new Student(2, Grade.SECOND, 80),
new Student(3, Grade.FIRST, 100),
new Student(4, Grade.FIRST, 78),
new Student(5, Grade.FIRST, 92)
);
List<Integer> studentIds = students.stream()
.filter(student -> student.getGrade().equals(Grade.FIRST))
.sorted(Comparator.comparingInt(Student::getScore))
.map(Student::getId)
.collect(Collectors.toList());
}
下面我們詳解以下stream。
3.1 什么是stream
? ? ? ?Stream 不是集合元素,它不是數據結構并不保存數據,它是有關算法和計算的,它更像一個高級版本的Iterator。
3.2 stream特點
- Iterator,用戶只能顯式地一個一個遍歷元素并對其執行某些操作;Stream,用戶只要給出需要對其包含的元素執行什么操作,比如 “過濾掉長度大于 10 的字符串”、 “獲取每個字符串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的數據轉換。
- Stream 就如同一個Iterator,單向,不可往復,數據只能遍歷一次,遍歷過一次后即用盡了,就好比流水從面前流過,一去不復返。
- Stream 可以并行化操作,Iterator只能命令式地、串行化操作。顧名思義,當使用串行方式去遍歷時,每個 item 讀完后再讀下一個 item。而使用并行去遍歷時,數據會被分成多個段,其中每一個都在不同的線程中處理,然后將結果一起輸出。 Stream 的并行操作依賴于 Java7 中引入的 Fork/Join 框架來拆分任務和加速處理過程。
3.3 stream的構成
? ? ? ?當我們使用一個流的時候,通常包括三個基本步驟:獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結果,每次轉換原有 Stream 對象不改變,返回一
個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道,如下圖所示。
3.3.1生成stream source的方式
-
從Collection 和數組生成:
- Collection.stream()
- Collection.parallelStream()
- Arrays.stream(T array)
- Stream.of(T t)
-
從BufferedReader
- java.io.BufferedReader.lines()
-
靜態工廠
- java.util.stream.IntStream.range()
- java.nio.file.Files.walk()
自己構建
java.util.Spliterator-
其它
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
3.3.2 stream的操作類型
- 中間操作(Intermediate Operation):一個流可以后面跟隨零個或多個 intermediate操作。其目的主要是打開流,做出某種程度的數據映射/過濾,然后返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅調用到這類方法,并沒有真正開始流的遍歷。
- 終止操作(Terminal Operation):一個流只能有一個 terminal 操作,當這個操作執行后,流就被使用“光”了,無法再被操作。所以這必定是流的最后一個操作。 Terminal 操作的執行,才會真正開始流的遍歷,并且會生成一個結果。
中間操作(Intermediate Operation)又可以分為兩種類型:
- 無狀態操作(Stateless Operation):操作是無狀態的,不需要知道集合中其他元素的狀態,每個元素之間是相互獨立的,比如map()、 filter()等操作。
- 有狀態操作(Stateful Operation):有狀態操作,操作是需要知道集合中其他元素的狀態才能進行的,比如sort()、 distinct()。
終止操作(Terminal Operation)從邏輯上可以分為兩種: - 短路操作(short-circuiting):短路操作是指不需要處理完所有元素即可結束整個過程
- 非短路操作(non-short-circuiting):非短路操作是需要處理完所有元素之后才能結束整個過程。
3.3.3 stream 的使用
? ? ? ?簡單說,對 Stream 的使用就是實現一個 filter-map-reduce 過程,產生一個最終結果,或者導致一個副作用。
? ? ? ?構造流的幾種常見方法:
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
? ? ? ?需要注意的是,對于基本數值型,目前有三種對應的包裝類型 Stream:
IntStream、 LongStream、 DoubleStream。當然我們也可以用 Stream、 Stream >、 Stream,但是 boxing 和unboxing 會很耗時,所以特別為這三種基本數值型提供了對應的 Stream。
? ? ? ?Java 8 中還沒有提供其它數值型 Stream,因為這將導致擴增的內容較多。而常規的數值型聚合運算可以通過上面三種 Stream 進行。
? ? ? ? 數值流的構造
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
? ? ? 流轉換成其他的數據結構
Stream<String> stream = Stream.<String>of(new String[]{"1", "2", "3"});
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
String str = stream.collect(Collectors.joining());
System.out.println(str);
注意,一個 Stream 只可以使用一次,上面的代碼為了簡潔而重復使用了數次。
? ? ? 流的典型用法
- map/flatmap
? ? ? 我們先來看 map。如果你熟悉 scala 這類函數式語言,對這個方法應該很了解,它的作用就是把 input Stream的每一個元素,映射成 output Stream 的另外一個元素。
Stream<String> stream = Stream.<String>of(new String[]{"a", "b", "c"});
stream.map(String::toUpperCase).forEach(System.out::println);
? ? ??這段代碼把所有的字母轉換為大寫。 map 生成的是個 1:1 映射,每個輸入元素,都按照規則轉換成為另外一個元素。還有一些場景,是一對多映射關系的,這時需要 flatMap:
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<Integer> mapStream = inputStream.map(List::size);
Stream<Integer> flatMapStream = inputStream.flatMap(Collection::stream);
- filter
? ? ? filter 對原始 Stream 進行某項測試,通過測試的元素被留下來生成一個新Stream。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).filter(n -> n<3).forEach(System.out::println);
將小于3的數字留下來。
- forEach
? ? ?forEach 是 terminal 操作,因此它執行后,Stream 的元素就被“消費”掉了,你無法對一個 Stream 進行兩次terminal 運算。下面的代碼會報錯。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Stream stream = Arrays.stream(nums);
stream.forEach(System.out::print);
stream.forEach(System.out::print);
? ? ?相反,具有相似功能的 intermediate 操作 peek 可以達到上述目的。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Stream stream = Arrays.stream(nums);
stream.peek(System.out::println)
.peek(data-> System.out.println("重復利用:"+data))
.collect(Collectors.toList());
?? ? ?forEach 不能修改自己包含的本地變量值,也不能用 break/return 之類的關鍵字提前結束循環。下面的代碼還是打印出所有元素,并不會提前返回。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).forEach(integer -> {
System.out.print(integer);
return;
});
?? ? ?forEach和常規和常規for循環的差異不涉及到性能,它們僅僅是函數式風格與傳統循環的差異不涉及到性能,它們僅僅是函數式風格與傳統Java風格的差別。風格的差別。
- reduce
?? ? ?這個方法的主要作用是把 Stream 元素組合起來。它提供一個起始值(種子),然后依照運算規則(BinaryOperator),和前面 Stream 的第一個、第二個、第 n 個元素組合。從這個意義上說,字符串拼接、數值的 sum、 min、 max、 average 都是特殊的 reduce。例如 Stream 的 sum 就相當于:
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Integer sum = Arrays.stream(nums).reduce(0, (integer, integer2) ->
integer+integer2);
System.out.println(sum);
?? ? ?也有沒有起始值的情況,這時會把 Stream 的前面兩個元素組合起來,返回的是 Optional。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
// 有初始值
Integer sum = Arrays.stream(nums).reduce(0, Integer::sum);
// 無初始值
Integer sum1 = Arrays.stream(nums).reduce(Integer::sum).get();
- limit / skip
?? ? ?limit 返回 Stream 的前面 n 個元素;skip 則是扔掉前 n 個元素。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).limit(3).forEach(System.out::print);
System.out.println();
Arrays.stream(nums).skip(2).forEach(System.out::print);
- sorted
?? ? ?對 Stream 的排序通過 sorted 進行,它比數組的排序更強之處在于你可以首先對 Stream 進行各類 map、 filter、limit、 skip 甚至 distinct 來減少元素數量后,再排序,這能幫助程序明顯縮短執行時間。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).sorted((i1, i2) ->
i2.compareTo(i1)).limit(3).forEach(System.out::print);
System.out.println();
Arrays.stream(nums).sorted((i1, i2) ->
i2.compareTo(i1)).skip(2).forEach(System.out::print);
- min/max/distinct
Integer[] nums = new Integer[]{1, 2, 2, 3, 4, 5, 5, 6};
System.out.println(Arrays.stream(nums).min(Comparator.naturalOrder()).get());
System.out.println(Arrays.stream(nums).max(Comparator.naturalOrder()).get());
Arrays.stream(nums).distinct().forEach(System.out::print);
- match
Stream 有三個 match 方法,從語義上說:
?? ? ?-- allMatch:Stream 中全部元素符合傳入的 predicate,返回 true
?? ? ?-- anyMatch:Stream 中只要有一個元素符合傳入的 predicate,返回 true
?? ? ?-- noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true
?? ? ?它們都不是要遍歷全部元素才能返回結果。例如 allMatch 只要一個元素不滿足條件,就 skip 剩下的所有元素,返回 false。
Integer[] nums = new Integer[]{1, 2, 2, 3, 4, 5, 5, 6};
System.out.println(Arrays.stream(nums).allMatch(integer -> integer < 7));
System.out.println(Arrays.stream(nums).anyMatch(integer -> integer < 2));
System.out.println(Arrays.stream(nums).noneMatch(integer -> integer < 0));
?? ? ?用Collection來進行reduction操作:
?? ? ?java.util.stream.Collectors 類的主要作用就是輔助進行各類有用的 reduction 操作,例如轉變輸出為 Collection,把Stream 元素進行歸組。
- groupingBy/partitioningBy
例如對上面的Student進行按年級進行分組:
final Collection<Student> students = Arrays.asList(
new Student(1, Grade.FIRST, 60),
new Student(2, Grade.SECOND, 80),
new Student(3, Grade.FIRST, 100)
);
// 按年級進行分組
students.stream().collect(Collectors.groupingBy(Student::getGrade)).forEach(((grade,
students1) -> {
System.out.println(grade);
students1.forEach(student ->
System.out.println(student.getId()+","+student.getGrade()+","+student.getScore()));
}));
打印結果:
例如對上面的Student進行按分數段進行分組:
students.stream().collect(Collectors.partitioningBy(student -> student.getScore()
<=60)).forEach(((grade, students1) -> {
System.out.println(grade);
students1.forEach(student ->
System.out.println(student.getId()+","+student.getGrade()+","+student.getScore()));
}));
parallelStream
parallelStream其實就是一個并行執行的流.它通過默認的ForkJoinPool,可以提高你的多線程任務的速度。
Arrays.stream(nums).parallel().forEach(System.out::print);
System.out.println(Arrays.stream(nums).parallel().reduce(Integer::sum).get());
System.out.println();
Arrays.stream(nums).forEach(System.out::print);
System.out.println(Arrays.stream(nums).reduce(Integer::sum).get());
parallelStream要注意的問題
?? ? ?parallelStream底層是使用的ForkJoin。而ForkJoin里面的線程是通過ForkJoinPool來運行的,Java 8為ForkJoinPool添加了一個通用線程池,這個線程池用來處理那些沒有被顯式提交到任何線程池的任務。它是ForkJoinPool類型上的一個靜態元素。它擁有的默認線程數量等于運行計算機上的處理器數量,所以這里就出現了這個java進程里所有使用parallelStream的地方實際上是公用的同一個ForkJoinPool。 parallelStream提供了更簡單的并發執行的實現,但并不意味著更高的性能,它是使用要根據具體的應用場景。如果cpu資源緊張parallelStream不會帶來性能提升;如果存在頻繁的線程切換反而會降低性能。
3.4 steam總結
- 不是數據結構,它沒有內部存儲,它只是用操作管道從 source(數據結構、數組、 generator function、IO channel)抓取數據。
- 它也絕不修改自己所封裝的底層數據結構的數據。例如 Stream 的 filter 操作會產生一個不包含被過濾元素的新 Stream,而不是從 source 刪除那些元素。
- 所有 Stream 的操作必須以 lambda 表達式為參數。
- 惰性化,很多 Stream 操作是向后延遲的,一直到它弄清楚了最后需要多少數據才會開始,Intermediate操作永遠是惰性化的。
- 當一個 Stream 是并行化的,就不需要再寫多線程代碼,所有對它的操作會自動并行進行的。
4. Date/Time API
?? ? ?Java 8通過發布新的Date-Time API (JSR 310)來進一步加強對日期與時間的處理。對日期與時間的操作一直是Java程序員最痛苦的地方之一。標準的 java.util.Date以及后來的java.util.Calendar一點沒有改善這種情況(可以這么說,它們一定程度上更加復雜)。
?? ? ?這種情況直接導致了Joda-Time——一個可替換標準日期/時間處理且功能非常強大的Java API的誕生。 Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影響,并且吸取了其精髓。
- LocalDate類
LocaleDate只持有ISO-8601格式且無時區信息的日期部分
LocalDate date = LocalDate.now(); // 當前日期
date = date.plusDays(1); // 增加一天
date = date.plusMonths(1); // 增加一個月
date = date.minusDays(1); // 減少一天
date = date.minusMonths(1); // 減少一個月
System.out.println(date);
- LocalTime類
?? ? ?LocaleTime只持有ISO-8601格式且無時區信息的時間部分
LocalTime time = LocalTime.now(); // 當前時間
time = time.plusMinutes(1); // 增加一分鐘
time = time.plusSeconds(1); // 增加一秒
time = time.minusMinutes(1); // 減少一分鐘
time = time.minusSeconds(1); // 減少一秒
System.out.println(time); // ?
- LocalDateTime類和格式化
?? ? ?LocaleDateTime把LocaleDate與LocaleTime的功能合并起來,它持有的是ISO-8601格式無時區信息的日期與時間。
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd
HH:mm:ss"))); // 2018-12-18 21:13:07 ?????
- ZonedDateTime類
?? ? ?如果你需要特定時區的日期/時間,那么ZonedDateTime是你的選擇。它持有ISO-8601格式具具有時區信息的日期與時間。
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of(
"America/Los_Angeles" ) );
System.out.println(zonedDatetimeFromZone);
-Clock類
?? ? ?它通過指定一個時區,然后就可以獲取到當前的時刻,日期與時間。 Clock可以替換System.currentTimeMillis()與TimeZone.getDefault()。
final Clock utc = Clock.systemUTC(); // 協調世界時,又稱世界統一時間、世界標準時間、國際協調時間
final Clock shanghai = Clock.system(ZoneId.of("Asia/Shanghai")); // 上海
System.out.println(LocalDateTime.now(utc));
System.out.println(LocalDateTime.now(shanghai));
- Duration類
Duration使計算兩個日期間的不同變的十分簡單。
final LocalDateTime from = LocalDateTime.parse("2018-12-17 18:50:50", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
final LocalDateTime to = LocalDateTime.parse("2018-12-18 19:50:50", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
final Duration duration = Duration.between(from, to);
System.out.println("Duration in days: " + duration.toDays()); // 1
System.out.println("Duration in hours: " + duration.toHours()); // 25
5.重復注解
?? ? ?假設,現在有一個服務我們需要定時運行,就像Linux中的cron一樣,假設我們需要它在每周三的12點運行一次,那我們可能會定義一個注解,有兩個代表時間的屬性。
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
int dayOfWeek() default 1; //周幾
int hour() default 0; //幾點
}
?? ? ?所以我們可以給對應的服務方法上使用該注解,代表運行的時間:
public class ScheduleService {
//每周三的12點執行
@Schedule(dayOfWeek = 3, hour = 12)
public void start() {
//執行服務
}
}
?? ? ?那么如果我們需要這個服務在每周四的13點也需要運行一下,如果是JDK8之前,那么...尷尬了!你不能像下面的代碼,會編譯錯誤
public class ScheduleService {
//jdk中兩個相同的注解會報錯
@Schedule(dayOfWeek = 3, hour = 12)
@Schedule(dayOfWeek = 4, hour = 13)
public void start() {
//執行服務
}
}
?? ? ?那么如果是JDK8,你可以改一下注解的代碼,在自定義注解上加上@Repeatable元注解,并且指定重復注解的存儲注解(其實就是需要需要數組來存儲重復注解),這樣就可以解決上面的編譯報錯問題。
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(value = Schedule.Schedules.class)
public @interface Schedule {
int dayOfWeek() default 1;
int hour() default 0;
@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
Schedule[] value();
}
}
同時,反射相關的API提供了新的函數getAnnotationsByType()來返回重復注解的類型。
添加main方法:
public static void main(String[] args) {
try {
Method method = ScheduleService.class.getMethod("start");
for (Annotation annotation : method.getAnnotations()) {
System.out.println(annotation);
}
for (Schedule s : method.getAnnotationsByType(Schedule.class)) {
System.out.println(s.dayOfWeek() + "|" + s.hour());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
6.擴展注釋
注解就相當于一種標記,在程序中加了注解就等于為程序加了某種標記。
JDK8之前的注解只能加在:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE, // 類,接口,枚舉
/** Field declaration (includes enum constants) */
FIELD, // 類型變量
/** Method declaration */
METHOD, // 方法
/** Parameter declaration */
PARAMETER, // 方法參數
/** Constructor declaration */
CONSTRUCTOR, // 構造方法
/** Local variable declaration */
LOCAL_VARIABLE, // 局部變量
/** Annotation type declaration */
ANNOTATION_TYPE, // 注解類型
/** Package declaration */
PACKAGE // 包
}
JDK8中新增了兩種:
- TYPE_PARAMETER,表示該注解能寫在類型變量的聲明語句中。
- TYPE_USE,表示該注解能寫在使用類型的任何語句中
checkerframework中的各種校驗注解,比如:@Nullable, @NonNull等等。
public class GetStarted {
void sample() {
@NonNull Object ref = null;
}
}
7.更好的類型推薦機制
直接看代碼:
public class Value<T> {
public static<T> T defaultValue() {
return null;
}
public T getOrDefault(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
public static void main(String[] args) {
Value<String> value = new Value<>();
System.out.println(value.getOrDefault("22", Value.defaultValue()));
}
}
上面的代碼重點關注value.getOrDefault("22", Value.defaultValue()), 在JDK8中不會報錯,那么在JDK7中呢?
答案是會報錯: Wrong 2nd argument type. Found: 'java.lang.Object', required:
'java.lang.String' 。所以Value.defaultValue()的參數類型在JDK8中可以被推測出,所以就不必明確給出。
8.參數名字保存在字節碼中
?? ? ?先來想一個問題:JDK8之前,怎么獲取一個方法的參數名列表?
?? ? ?在JDK7中一個Method對象有下列方法:
Method.getParameterAnnotations()
獲取方法參數上的注解
Method.getParameterTypes()
獲取方法的參數類型列表
?? ? ?但是沒有能夠獲取到方法的參數名字列表!
?? ? ?在JDK8中增加了兩個方法:
Method.getParameters()
獲取參數名字列表
Method.getParameterCount()
獲取參數名字個數
?? ? ?用法:
public class ParameterNames {
public void test(String name, String address) {
}
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod("test", String.class,
String.class);
for (Parameter parameter : method.getParameters()) {
System.out.println(parameter.getName());
}
System.out.println(method.getParameterCount());
}
}
結果:
從結果可以看出輸出的參數個數正確,但是名字不正確!需要在編譯 編譯時增加–parameters參數后再運行。
9. 異步調用 CompletableFuture
當我們Javer說異步調用時,我們自然會想到Future,比如:
public class FutureDemo {
/**
* 異步進行一個計算
* @param args
*/
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
int sum=0;
System.out.println("正在計算...");
for (int i=0; i<100; i++) {
sum = sum + i;
}
//模擬等待
Thread.sleep(TimeUnit.SECONDS.toSeconds(3));
System.out.println("算完了 ");
return sum;
}
});
try {
System.out.println("result:" + result.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("事情都做完了...");
executor.shutdown();
}
}
如果想實現異步計算完成之后,立馬能拿到這個結果且繼續異步做其他事情呢?這個問題就是一個線程依賴另外一個線程,這個時候Future就不方便,我們來看一下CompletableFuture的實現:
public class FutureDemo {
/**
* 異步進行一個計算
* @param args
*/
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture result = CompletableFuture.supplyAsync(() -> {
int sum=0;
System.out.println("正在計算...");
for (int i=0; i<100; i++) {
sum = sum + i;
}
try {
Thread.sleep(TimeUnit.SECONDS.toSeconds(3));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"算完了");
return sum;
}, executor).thenApplyAsync(sum -> {
System.out.println(Thread.currentThread().getName()+"打印"+sum);
return sum;
}, executor);
System.out.println("做其他事情...");
try {
System.out.println("result:" + result.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("事情都做完了...");
executor.shutdown();
}
}
只需要簡單的使用thenApplyAsync就可以實現了。
當然CompletableFuture還有很多其他的特性,我們下次單獨開個專題來講解。
9. Java虛擬機(JVM)新特性
PermGen空間被移除了,取而代之的是Metaspace(JEP 122)。 JVM選項-XX:PermSize與-XX:MaxPermSize分別被-XX:MetaSpaceSize與-XX:MaxMetaspaceSize所代替。虛擬機內容太多了,以后會出個專題,想深入了解虛擬機的讀者可留言,看的人多我就抓緊更新。
`