JDK8新特性學習總結
JDK1.8的新特性
1. 前言
JDK1.8已經發布很久了,在很多企業中都已經在使用。並且Spring5、SpringBoot2.0都推薦使用JDK1.8以上版本。所以我們必須與時俱進,擁抱變化。
Jdk8這個版本包含語言、編譯器、庫、工具和JVM等方面的十多個新特性。在本文中我們將學習以下方面的新特性:
- Lambda表示式
- 函式式介面
- 方法引用
- 介面的預設方法和靜態方法
- Optionals
- Streams
- 並行陣列
2. Lambda表示式
函數語言程式設計
Lambda 表示式,也可稱為閉包,它是推動 Java 8 釋出的最重要新特性。Lambda 允許把函式作為一個方法的引數(函式作為引數傳遞進方法中)。可以使程式碼變的更加簡潔緊湊。
2.1 基本語法:
(引數列表) -> {程式碼塊}
需要注意:
- 引數型別可省略,編譯器可以自己推斷
- 如果只有一個引數,圓括號可以省略
- 程式碼塊如果只是一行程式碼,大括號也可以省略
- 如果程式碼塊是一行,且是有結果的表示式,
return
可以省略
注意:事實上,把Lambda表示式可以看做是匿名內部類的一種簡寫方式。當然,前提是這個匿名內部類對應的必須是介面,而且介面中必須只有一個函式!Lambda表示式就是直接編寫函式的:引數列表、程式碼體、返回值等資訊,用函式來代替完整的匿名內部類
!
2.2 用法示例
示例1:多個引數
準備一個集合:
// 準備一個集合
List<Integer> list = Arrays.asList(10, 5, 25, -15, 20);
假設我們要對集合排序,我們先看JDK7的寫法,需要通過匿名內部類來構造一個Comparator
:
// Jdk1.7寫法
Collections.sort(list,new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(list);// [-15, 5, 10, 20, 25]
如果是jdk8,我們可以使用新增的集合API:sort(Comparator c)
方法,接收一個比較器,我們用Lambda來代替Comparator
的匿名內部類:
// Jdk1.8寫法,引數列表的資料型別可省略:
list.sort((i1,i2) -> { return i1 - i2;});
System.out.println(list);// [-15, 5, 10, 20, 25]
對比一下Comparator
中的compare()
方法,你會發現:這裡編寫的Lambda表示式,恰恰就是compare()
方法的簡寫形式,JDK8會把它編譯為匿名內部類。是不是簡單多了!
彆著急,我們發現這裡的程式碼塊只有一行程式碼,符合前面的省略規則,我們可以簡寫為:
// Jdk8寫法
// 因為程式碼塊是一個有返回值的表示式,可以省略大括號以及return
list.sort((i1,i2) -> i1 - i2);
示例2:單個引數
還以剛才的集合為例,現在我們想要遍歷集合中的元素,並且列印。
先用jdk1.7的方式:
// JDK1.7遍歷並列印集合
for (Integer i : list) {
System.out.println(i);
}
jdk1.8給集合新增了一個方法:foreach()
,接收一個對元素進行操作的函式:
// JDK1.8遍歷並列印集合,因為只有一個引數,所以我們可以省略小括號:
list.forEach(i -> System.out.println(i));
例項3:把Lambda賦值給變數
Lambda表示式的實質其實還是匿名內部類,所以我們其實可以把Lambda表示式賦值給某個變數。
// 將一個Lambda表示式賦值給某個介面:
Runnable task = () -> {
// 這裡其實是Runnable介面的匿名內部類,我們在編寫run方法。
System.out.println("hello lambda!");
};
new Thread(task).start();
不過上面的用法很少見,一般都是直接把Lambda作為引數。
示例4:隱式final
Lambda表示式的實質其實還是匿名內部類,而匿名內部類在訪問外部區域性變數時,要求變數必須宣告為final
!不過我們在使用Lambda表示式時無需宣告final
,這並不是說違反了匿名內部類的規則,因為Lambda底層會隱式的把變數設定為final
,在後續的操作中,一定不能修改該變數:
正確示範:
// 定義一個區域性變數
int num = -1;
Runnable r = () -> {
// 在Lambda表示式中使用區域性變數num,num會被隱式宣告為final
System.out.println(num);
};
new Thread(r).start();// -1
錯誤案例:
// 定義一個區域性變數
int num = -1;
Runnable r = () -> {
// 在Lambda表示式中使用區域性變數num,num會被隱式宣告為final,不能進行任何修改操作
System.out.println(num++);
};
new Thread(r).start();//報錯
3. 函式式介面
經過前面的學習,相信大家對於Lambda表示式已經有了初步的瞭解。總結一下:
- Lambda表示式是介面的匿名內部類的簡寫形式
- 介面必須滿足:內部只有一個函式
其實這樣的介面,我們稱為函式式介面,我們學過的Runnable
、Comparator
都是函式式介面的典型代表。但是在實踐中,函式介面是非常脆弱的,只要有人在介面裡新增多一個方法,那麼這個介面就不是函式介面了,就會導致編譯失敗。Java 8提供了一個特殊的註解@FunctionalInterface
來克服上面提到的脆弱性並且顯示地表明函式介面。而且jdk8版本中,對很多已經存在的介面都新增了@FunctionalInterface
註解,例如Runnable
介面:
另外,Jdk8預設提供了一些函式式介面供我們使用:
3.1 Function型別介面
@FunctionalInterface
public interface Function<T, R> {
// 接收一個引數T,返回一個結果R
R apply(T t);
}
Function代表的是有引數,有返回值的函式。還有很多類似的Function介面:
介面名 | 描述 |
---|---|
BiFunction<T,U,R> | 接收兩個T和U型別的引數,並且返回R型別結果的函式 |
DoubleFunction<R> | 接收double型別引數,並且返回R型別結果的函式 |
IntFunction<R> | 接收int型別引數,並且返回R型別結果的函式 |
LongFunction<R> | 接收long型別引數,並且返回R型別結果的函式 |
ToDoubleFunction<T> | 接收T型別引數,並且返回double型別結果 |
ToIntFunction<T> | 接收T型別引數,並且返回int型別結果 |
ToLongFunction<T> | 接收T型別引數,並且返回long型別結果 |
DoubleToIntFunction | 接收double型別引數,返回int型別結果 |
DoubleToLongFunction | 接收double型別引數,返回long型別結果 |
看出規律了嗎?這些都是一類函式介面,在Function基礎上衍生出的,要麼明確了引數不確定返回結果,要麼明確結果不知道引數型別,要麼兩者都知道。
3.2 Consumer系列
@FunctionalInterface
public interface Consumer<T> {
// 接收T型別引數,不返回結果
void accept(T t);
}
Consumer系列與Function系列一樣,有各種衍生介面,這裡不一一列出了。不過都具備類似的特徵:那就是不返回任何結果。
3.3 Predicate系列
@FunctionalInterface
public interface Predicate<T> {
// 接收T型別引數,返回boolean型別結果
boolean test(T t);
}
Predicate系列引數不固定,但是返回的一定是boolean型別。
3.4 Supplier系列
@FunctionalInterface
public interface Supplier<T> {
// 無需引數,返回一個T型別結果
T get();
}
Supplier系列,英文翻譯就是“供應者”,顧名思義:只產出,不收取。所以不接受任何引數,返回T型別結果。
4. 方法引用
方法引用使得開發者可以將已經存在的方法作為變數來傳遞使用。方法引用可以和Lambda表示式配合使用。
4.1 語法:
總共有四類方法引用:
語法 | 描述 |
---|---|
類名::靜態方法名 | 類的靜態方法的引用 |
類名::非靜態方法名 | 類的非靜態方法的引用 |
例項物件::非靜態方法名 | 類的指定例項物件的非靜態方法引用 |
類名::new | 類的構造方法引用 |
4.2 示例
首先我們編寫一個集合工具類,提供一個方法:
public class CollectionUtil{
/**
* 利用function將list集合中的每一個元素轉換後形成新的集合返回
* @param list 要轉換的源集合
* @param function 轉換元素的方式
* @param <T> 源集合的元素型別
* @param <R> 轉換後的元素型別
* @return
*/
public static <T,R> List<R> convert(List<T> list, Function<T,R> function){
List<R> result = new ArrayList<>();
list.forEach(t -> result.add(function.apply(t)));
return result;
}
}
可以看到這個方法接收兩個引數:
List<T> list
:需要進行轉換的集合Function<T,R>
:函式介面,接收T型別,返回R型別。用這個函式介面對list中的元素T進行轉換,變為R型別
接下來,我們看具體案例:
4.2.1 類的靜態方法引用
List<Integer> list = Arrays.asList(1000, 2000, 3000);
我們需要把這個集合中的元素轉為十六進位制儲存,需要呼叫Integer.toHexString()
方法:
public static String toHexString(int i) {
return toUnsignedString0(i, 4);
}
這個方法接收一個 i 型別,返回一個String
型別,可以用來構造一個Function
的函式介面:
我們先按照Lambda原始寫法,傳入的Lambda表示式會被編譯為Function
介面,介面中通過Integer.toHexString(i)
對原來集合的元素進行轉換:
// 通過Lambda表示式實現
List<String> hexList = CollectionUtil.convert(list, i -> Integer.toHexString(i));
System.out.println(hexList);// [3e8, 7d0, bb8]
上面的Lambda表示式程式碼塊中,只有對Integer.toHexString()
方法的引用,沒有其它程式碼,因此我們可以直接把方法作為引數傳遞,由編譯器幫我們處理,這就是靜態方法引用:
// 類的靜態方法引用
List<String> hexList = CollectionUtil.convert(list, Integer::toHexString);
System.out.println(hexList);// [3e8, 7d0, bb8]
4.2.2 類的非靜態方法引用
接下來,我們把剛剛生成的String
集合hexList
中的元素都變成大寫,需要藉助於String類的toUpperCase()方法:
public String toUpperCase() {
return toUpperCase(Locale.getDefault());
}
這次是非靜態方法,不能用類名呼叫,需要用例項物件,因此與剛剛的實現有一些差別,我們接收集合中的每一個字串s
。但與上面不同然後s
不是toUpperCase()
的引數,而是呼叫者:
// 通過Lambda表示式,接收String資料,呼叫toUpperCase()
List<String> upperList = CollectionUtil.convert(hexList, s -> s.toUpperCase());
System.out.println(upperList);// [3E8, 7D0, BB8]
因為程式碼體只有對toUpperCase()
的呼叫,所以可以把方法作為引數引用傳遞,依然可以簡寫:
// 類的成員方法
List<String> upperList = CollectionUtil.convert(hexList, String::toUpperCase);
System.out.println(upperList);// [3E8, 7D0, BB8]
4.2.3 指定例項的非靜態方法引用
下面一個需求是這樣的,我們先定義一個數字Integer num = 2000
,然後用這個數字和集合中的每個數字進行比較,比較的結果放入一個新的集合。比較物件,我們可以用Integer
的compareTo
方法:
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
先用Lambda實現,
List<Integer> list = Arrays.asList(1000, 2000, 3000);
// 某個物件的成員方法
Integer num = 2000;
List<Integer> compareList = CollectionUtil.convert(list, i -> num.compareTo(i));
System.out.println(compareList);// [1, 0, -1]
與前面類似,這裡Lambda的程式碼塊中,依然只有對num.compareTo(i)
的呼叫,所以可以簡寫。但是,需要注意的是,這次方法的呼叫者不是集合的元素,而是一個外部的區域性變數num
,因此不能使用 Integer::compareTo
,因為這樣是無法確定方法的呼叫者。要指定呼叫者,需要用 物件::方法名
的方式:
// 某個物件的成員方法
Integer num = 2000;
List<Integer> compareList = CollectionUtil.convert(list, num::compareTo);
System.out.println(compareList);// [1, 0, -1]
4.2.4 建構函式引用
最後一個場景:把集合中的數字作為毫秒值,構建出Date
物件並放入集合,這裡我們就需要用到Date的建構函式:
/**
* @param date the milliseconds since January 1, 1970, 00:00:00 GMT.
* @see java.lang.System#currentTimeMillis()
*/
public Date(long date) {
fastTime = date;
}
我們可以接收集合中的每個元素,然後把元素作為Date
的建構函式引數:
// 將數值型別集合,轉為Date型別
List<Date> dateList = CollectionUtil.convert(list, i -> new Date(i));
// 這裡遍歷元素後需要列印,因此直接把println作為方法引用傳遞了
dateList.forEach(System.out::println);
上面的Lambda表示式實現方式,程式碼體只有new Date()
一行程式碼,因此也可以採用方法引用進行簡寫。但問題是,建構函式沒有名稱,我們只能用new
關鍵字來代替:
// 構造方法
List<Date> dateList = CollectionUtil.convert(list, Date::new);
dateList.forEach(System.out::println);
注意兩點:
- 上面程式碼中的System.out::println 其實是 指定物件System.out的非靜態方法println的引用
- 如果建構函式有多個,可能無法區分導致傳遞失敗
5. 介面的預設方法和靜態方法
Java 8使用兩個新概念擴充套件了介面的含義:預設方法和靜態方法。
5.1 預設方法
預設方法使得開發者可以在 不破壞二進位制相容性的前提下,往現存介面中新增新的方法,即不強制那些實現了該介面的類也同時實現這個新加的方法。
預設方法和抽象方法之間的區別在於抽象方法需要實現,而預設方法不需要。介面提供的預設方法會被介面的實現類繼承或者覆寫,例子程式碼如下:
private interface Defaulable {
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}
Defaulable介面使用關鍵字default定義了一個預設方法notRequired()。DefaultableImpl類實現了這個介面,同時預設繼承了這個介面中的預設方法;OverridableImpl類也實現了這個介面,但覆寫了該介面的預設方法,並提供了一個不同的實現。
5.2 靜態方法
Java 8帶來的另一個有趣的特性是在介面中可以定義靜態方法,我們可以直接用介面呼叫這些靜態方法。例子程式碼如下:
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
下面的程式碼片段整合了預設方法和靜態方法的使用場景:
public static void main( String[] args ) {
// 呼叫介面的靜態方法,並且傳遞DefaultableImpl的建構函式引用來構建物件
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
// 呼叫介面的靜態方法,並且傳遞OverridableImpl的建構函式引用來構建物件
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
這段程式碼的輸出結果如下:
Default implementation
Overridden implementation
由於JVM上的預設方法的實現在位元組碼層面提供了支援,因此效率非常高。預設方法允許在不打破現有繼承體系的基礎上改進介面。該特性在官方庫中的應用是:給java.util.Collection
介面新增新方法,如stream()
、parallelStream()
、forEach()
和removeIf()
等等。
儘管預設方法有這麼多好處,但在實際開發中應該謹慎使用:在複雜的繼承體系中,預設方法可能引起歧義和編譯錯誤。如果你想了解更多細節,可以參考官方文件。
6. Optional
Java應用中最常見的bug就是空值異常。
Optional
僅僅是一個容器,可以存放T型別的值或者null
。它提供了一些有用的介面來避免顯式的null
檢查,可以參考Java 8官方文件瞭解更多細節。
接下來看一點使用Optional的例子:可能為空的值或者某個型別的值:
Optional< String > fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
如果Optional
例項持有一個非空值,則isPresent()
方法返回true
,否則返回false
;如果Optional
例項持有null
,orElseGet()
方法可以接受一個lambda表示式生成的預設值;map()
方法可以將現有的Optional
例項的值轉換成新的值;orElse()
方法與orElseGet()
方法類似,但是在持有null的時候返回傳入的預設值,而不是通過Lambda來生成。
上述程式碼的輸出結果如下:
Full Name is set? false
Full Name: [none]
Hey Stranger!
再看下另一個簡單的例子:
Optional< String > firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
這個例子的輸出是:
First Name is set? true
First Name: Tom
Hey Tom!
如果想了解更多的細節,請參考官方文件。
7. Streams
新增的Stream API(java.util.stream)將生成環境的函數語言程式設計引入了Java庫中。這是目前為止最大的一次對Java庫的完善,以便開發者能夠寫出更加有效、更加簡潔和緊湊的程式碼。
Steam API極大得簡化了集合操作(後面我們會看到不止是集合),首先看下這個叫Task的類:
public class Streams {
private enum Status {
OPEN, CLOSED
};
private static final class Task {
private final Status status;
private final Integer points;
Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}
public Integer getPoints() {
return points;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}
Task類有一個points屬性,另外還有兩種狀態:OPEN或者CLOSED。現在假設有一個task集合:
final Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);
首先看一個問題:在這個task集合中一共有多少個OPEN狀態的?計算出它們的points屬性和。在Java 8之前,要解決這個問題,則需要使用foreach迴圈遍歷task集合;但是在Java 8中可以利用steams解決:包括一系列元素的列表,並且支援順序和並行處理。
// 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 );
執行這個方法的控制檯輸出是:
Total points: 18
這裡有很多知識點值得說。首先,tasks
集合被轉換成steam
表示;其次,在steam
上的filter
操作會過濾掉所有CLOSED
的task
;第三,mapToInt
操作基於tasks
集合中的每個task
例項的Task::getPoints
方法將task
流轉換成Integer
集合;最後,通過sum
方法計算總和,得出最後的結果。
在學習下一個例子之前,還需要記住一些steams(點此更多細節)的知識點。Steam之上的操作可分為中間操作和晚期操作。
中間操作會返回一個新的steam——執行一箇中間操作(例如filter)並不會執行實際的過濾操作,而是建立一個新的steam,並將原steam中符合條件的元素放入新建立的steam。
晚期操作(例如forEach或者sum),會遍歷steam並得出結果或者附帶結果;在執行晚期操作之後,steam處理線已經處理完畢,就不能使用了。在幾乎所有情況下,晚期操作都是立刻對steam進行遍歷。
steam的另一個價值是創造性地支援並行處理(parallel processing)。對於上述的tasks集合,我們可以用下面的程式碼計算所有task的points之和:
// Calculate total points of all tasks
final double totalPoints = tasks
.stream()
.parallel()
.map( task -> task.getPoints() ) // or map( Task::getPoints )
.reduce( 0, Integer::sum );
System.out.println( "Total points (all tasks): " + totalPoints );
這裡我們使用parallel方法並行處理所有的task,並使用reduce方法計算最終的結果。控制檯輸出如下:
Total points(all tasks): 26.0
對於一個集合,經常需要根據某些條件對其中的元素分組。利用steam提供的API可以很快完成這類任務,程式碼如下:
// Group tasks by their status
final Map< Status, List< Task > > map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
控制檯的輸出如下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
最後一個關於tasks集合的例子問題是:如何計算集合中每個任務的點數在集合中所佔的比重,具體處理的程式碼如下:
// Calculate the weight of each tasks (as percent of total points)
final Collection< String > result = tasks
.stream() // Stream< String >
.mapToInt( Task::getPoints ) // IntStream
.asLongStream() // LongStream
.mapToDouble( points -> points / totalPoints ) // DoubleStream
.boxed() // Stream< Double >
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" ) // Stream< String>
.collect( Collectors.toList() ); // List< String >
System.out.println( result );
控制檯輸出結果如下:
[19%, 50%, 30%]
最後,正如之前所說,Steam API不僅可以作用於Java集合,傳統的IO操作(從檔案或者網路一行一行得讀取資料)可以受益於steam處理,這裡有一個小例子:
final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
Stream的方法onClose()
返回一個等價的有額外控制程式碼的Stream,當Stream的close()
方法被呼叫的時候這個控制程式碼會被執行。Stream API、Lambda表示式還有介面預設方法和靜態方法支援的方法引用,是Java 8對軟體開發的現代正規化的響應。
8. 並行陣列
Java8版本新增了很多新的方法,用於支援並行陣列處理。最重要的方法是parallelSort()
,可以顯著加快多核機器上的陣列排序。下面的例子論證了parallexXxx系列的方法:
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
上述這些程式碼使用parallelSetAll()方法生成20000個隨機數,然後使用parallelSort()方法進行排序。這個程式會輸出亂序陣列和排序陣列的前10個元素。上述例子的程式碼輸出的結果是:
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
上述這些程式碼使用parallelSetAll()方法生成20000個隨機數,然後使用parallelSort()方法進行排序。這個程式會輸出亂序陣列和排序陣列的前10個元素。上述例子的程式碼輸出的結果是:
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
相關文章
- JDK8 新特性學習筆記JDK筆記
- JDK8新特性JDK
- JDK8的新特性JDK
- JDK8新特性詳解JDK
- JDK8新特性之stream()JDK
- JDK8新特性之Stream流JDK
- JDK8新特性詳解(二)JDK
- JDK8新特性(4)—— stream 流JDK
- JDK8新特性詳解(一)JDK
- html5新特性總結HTML
- ES6新特性總結
- React 16 新特性使用總結React
- JDK1.8新特性總結JDK
- css3新特性總結CSSS3
- JDK8新特性-你瞭解多少JDK
- JDK8新特性之函式式介面JDK函式
- PHP 各個版本新特性總結PHP
- iOS 12正式版新特性總結iOS
- Java 新特性總結——簡單實用Java
- Java8常用的新特性總結Java
- JDK 1.8 新特性學習(Stream)JDK
- Java1.8新特性學習Java
- 學習總結
- Automatic Reference Counting(ARC)特性學習(iOS5新特性學習之五)iOS
- 【Java】jdk1.8新特性及用法總結JavaJDK
- JDK 1.5 - 1.8 各版本的新特性總結JDK
- react-router v6新特性總結React
- JDK11新特性學習(二)JDK
- JDK11新特性學習(一)JDK
- C++ 11 新特性 nullptr 學習C++Null
- JDK8 String類知識總結JDK
- JDK8特性之LocalDateTimeJDKLDA
- Oracle特性總結Oracle
- 總結:JDK1.5-JDK1.8各個新特性JDK
- ConstraintLayout 學習總結AI
- BOM學習總結
- tkinter學習總結
- vue學習總結Vue