android學習之路(二)----java8


Java8新特性

一、lambda語法
1.概述
    lambda(閉包)以及虛擬擴展方法(default method)
2.函數式接口
    函數式接口(functional interface 也叫功能性接口,其實是同一個東西)。簡單來說,函數式接口是只包含一個方法的接口。比如Java標准庫中的java.lang.Runnable和java.util.Comparator都是典型的函數式接口。
    java 8提供 @FunctionalInterface作為注解,這個注解是非必須的,只要接口符合函數式接口的標准(即只包含一個方法的接口),虛擬機會自動判斷,但最好在接口上使用注解@FunctionalInterface進行聲明,以免團隊的其他人員錯誤地往接口中添加新的方法。
    Java中的lambda無法單獨出現,它需要一個函數式接口來盛放,lambda表達式方法體其實就是函數接口的實現,下面講到語法會講到
3.Lambda語法
    包含三個部分
3.1 一個括號內用逗號分隔的形式參數,參數是函數式接口里面方法的參數
3.2 一個箭頭符號:->
3.3 方法體,可以是表達式和代碼塊,方法體函數式接口里面方法的實現,如果是代碼塊,則必須用{}來包裹起來,且需要一個return 返回值,但有個例外,若函數式接口里面方法返回值是void,則無需{}
    總體看起來像這樣
(parameters) -> expression 或者 (parameters) -> { statements; }

public class TestLambda {
    public static void runThreadUseLambda() {
        //Runnable是一個函數接口,只包含了有個無參數的,返回void的run方法;
        //所以lambda表達式左邊沒有參數,右邊也沒有return,只是單純的打印一句話
        new Thread(() ->System.out.println("lambda實現的線程")).start(); 
    }
    public static void runThreadUseInnerClass() {
        //這種方式就不多講了,以前舊版本比較常見的做法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("內部類實現的線程");
            }
        }).start();
    }
    public static void main(String[] args) {
        TestLambda.runThreadUseLambda();
        TestLambda.runThreadUseInnerClass();
    }
}

3.4 方法引用
    其實是lambda表達式的一個簡化寫法,所引用的方法其實是lambda表達式的方法體實現,語法也很簡單,左邊是容器(可以是類名,實例名),中間是”::”,右邊是相應的方法名。如下所示:

ObjectReference::methodName

一般方法的引用格式是
1. 如果是靜態方法,則是ClassName::methodName。如 Object ::equals
2. 如果是實例方法,則是Instance::methodName。
Object obj=new Object();obj::equals;
3. 構造函數.則是ClassName::new
再來看一個完整的例子,方便理解

public class TestMethodReference {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setLayout(new FlowLayout());
        frame.setVisible(true);
        JButton button1 = new JButton("點我!");
        JButton button2 = new JButton("也點我!");
        frame.getContentPane().add(button1);
        frame.getContentPane().add(button2);
        // 這里addActionListener方法的參數是ActionListener,是一個函數式接口
        // 使用lambda表達式方式
        button1.addActionListener(e -> {
            System.out.println("這里是Lambda實現方式");
        });
        // 使用方法引用方式
        button2.addActionListener(Test::doSomething);
        frame.show();
    }
    /** * 這里是函數式接口ActionListener的實現方法 * @param e */
    public static void doSomething(ActionEvent e) {
        System.out.println("這里是方法引用實現方式");
    }
}

    可以看出,doSomething方法就是lambda表達式的實現,這樣的好處就是,如果你覺得lambda的方法體會很長,影響代碼可讀性,方法引用就是個解決辦法
4.總結
    以上就是lambda表達式語法的全部內容了,相信大家對lambda表達式都有一定的理解了,但只是代碼簡潔了這個好處的話,並不能打動很多觀眾,java 8也不會這么令人期待,其實java 8引入lambda迫切需求是因為lambda 表達式能簡化集合上數據的多線程或者多核的處理,提供更快的集合處理速度 ,這個后續會講到,關於JEP126的這一特性,將分3部分,之所以分開,是因為這一特性可寫的東西太多了,這部分讓讀者熟悉lambda表達式以及方法引用的語法和概念,第二部分則是虛擬擴展方法(default method)的內容,最后一部分則是大數據集合的處理,解開lambda表達式的最強作用的神秘面紗。敬請期待。。。。

二、深入解析默認方法(也稱為虛擬擴展方法或防護方法)
1. 什么是默認方法,為什么要有默認方法
    簡單說,就是接口可以有實現方法,而且不需要實現類去實現其方法。只需在方法名前面加個default關鍵字即可。
    為什么要有這個特性?首先,之前的接口是個雙刃劍,好處是面向抽象而不是面向具體編程,缺陷是,當需要修改接口時候,需要修改全部實現該接口的類,目前的java 8之前的集合框架沒有foreach方法,通常能想到的解決辦法是在JDK里給相關的接口添加新的方法及實現。然而,對於已經發布的版本,是沒法在給接口添加新方法的同時不影響已有的實現。所以引進的默認方法。他們的目的是為了解決接口的修改與現有的實現不兼容的問題。
    簡單的例子
一個接口A,Clazz類實現了接口A。

public interface A {
    default void foo(){
       System.out.println("Calling A.foo()");
    }
}

public class Clazz implements A {
    public static void main(String[] args){
       Clazz clazz = new Clazz();
       clazz.foo();//調用A.foo()
    }
}

    代碼是可以編譯的,即使Clazz類並沒有實現foo()方法。在接口A中提供了foo()方法的默認實現。
2.java 8抽象類與接口對比
    這一個功能特性出來后,很多同學都反應了,java 8的接口都有實現方法了,跟抽象類還有什么區別?其實還是有的,請看下表對比。。
這里寫圖片描述
3.多重繼承的沖突說明
    由於同一個方法可以從不同接口引入,自然而然的會有沖突的現象,默認方法判斷沖突的規則如下:
3.1.一個聲明在類里面的方法優先於任何默認方法(classes always win)
3.2.否則,則會優先選取最具體的實現,比如下面的例子 B重寫了A的hello方法。
    輸出結果是:Hello World from B
如果想調用A的默認函數,則用到新語法X.super.m(…),下面修改C類,實現A接口,重寫一個hello方法,如下所示:

public class C implements A{
    @Override
    public void hello(){
        A.super.hello();
    }
    public static void main(String[] args){
        new C().hello();
    }
}

輸出結果是:Hello World from A
4. 總結
    默認方法給予我們修改接口而不破壞原來的實現類的結構提供了便利,目前java 8的集合框架已經大量使用了默認方法來改進了,當我們最終開始使用Java 8的lambdas表達式時,提供給我們一個平滑的過渡體驗。也許將來我們會在API設計中看到更多的默認方法的應用。
三、lambda進階
1.概述
    lambda為java帶來閉包的概念,但是如果我們不在集合中使用它的話,就損失了很大價值。現有接口遷移成為lambda風格的問題已經通過default methods解決了,在這篇文章將深入解析Java集合里面的批量數據操作(bulk operation),解開lambda最強作用的神秘面紗。
2.關於JSR335
    JSR是Java Specification Requests的縮寫,意思是Java 規范請求,Java 8 版本的主要改進是 Lambda 項目(JSR 335),其目的是使 Java 更易於為多核處理器編寫代碼。JSR 335=lambda表達式+接口改進(默認方法)+批量數據操作。加上前面兩篇,我們已是完整的學習了JSR335的相關內容了。
3.外部VS內部迭代
    以前Java集合是不能夠表達內部迭代的,而只提供了一種外部迭代的方式,也就是for或者while循環。

List persons = asList(new Person("Joe"), new Person("Jim"), new Person("John"));
for (Person p :  persons) {
   p.setLastName("Doe");
}

    上面的例子是我們以前的做法,也就是所謂的外部迭代,循環是固定的順序循環。在現在多核的時代,如果我們想並行循環,不得不修改以上代碼。效率能有多大提升還說定,且會帶來一定的風險(線程安全問題等等)。
    要描述內部迭代,我們需要用到Lambda這樣的類庫,下面利用lambda和Collection.forEach重寫上面的循環

persons.forEach(p->p.setLastName("Doe"));

    現在是由jdk 庫來控制循環了,我們不需要關心last name是怎么被設置到每一個person對象里面去的,庫可以根據運行環境來決定怎么做,並行,亂序或者懶加載方式。這就是內部迭代,客戶端將行為p.setLastName當做數據傳入api里面。
    內部迭代其實和集合的批量操作並沒有密切的聯系,借助它我們感受到語法表達上的變化。真正有意思的和批量操作相關的是新的流(stream)API。新的java.util.stream包已經添加進JDK 8了。

4.Stream API
    流(Stream)僅僅代表着數據流,並沒有數據結構,所以他遍歷完一次之后便再也無法遍歷(這點在編程時候需要注意,不像Collection,遍歷多少次里面都還有數據),它的來源可以是Collection、array、io等等。
4.1中間與終點方法
    流作用是提供了一種操作大數據接口,讓數據操作更容易和更快。它具有過濾、映射以及減少遍歷數等方法,這些方法分兩種:中間方法和終端方法,“流”抽象天生就該是持續的,中間方法永遠返回的是Stream,因此如果我們要獲取最終結果的話,必須使用終點操作才能收集流產生的最終結果。區分這兩個方法是看他的返回值,如果是Stream則是中間方法,否則是終點方法。具體請參照Stream的api。
    簡單介紹下幾個中間方法(filter、map)以及終點方法(collect、sum)
4.1.1 Filter
    在數據流中實現過濾功能是首先我們可以想到的最自然的操作了。Stream接口暴露了一個filter方法,它可以接受表示操作的Predicate實現來使用定義了過濾條件的lambda表達式。

List persons = …
Stream personsOver18 = persons.stream().filter(p -> p.getAge() > 18);//過濾18歲以上的人

4.1.2 Map
假使我們現在過濾了一些數據,比如轉換對象的時候。Map操作允許我們執行一個Function的實現(Function<T,R>的泛型T,R分別表示執行輸入和執行結果),它接受入參並返回。首先,讓我們來看看怎樣以匿名內部類的方式來描述它:

Stream adult= persons
              .stream()
              .filter(p -> p.getAge() > 18)
              .map(new Function() {
                  @Override
                  public Adult apply(Person person) {
                     return new Adult(person);//將大於18歲的人轉為成年人
                  }
              });

現在,把上述例子轉換成使用lambda表達式的寫法:

Stream map = persons.stream()
                    .filter(p -> p.getAge() > 18)
                    .map(person -> new Adult(person));

4.1.3Count
    count方法是一個流的終點方法,可使流的結果最終統計,返回int,比如我們計算一下滿足18歲的總人數

int countOfAdult=persons.stream()
                       .filter(p -> p.getAge() > 18)
                       .map(person -> new Adult(person))
                       .count();

4.1.4Collect
    collect方法也是一個流的終點方法,可收集最終的結果

List adultList= persons.stream()
                       .filter(p -> p.getAge() > 18)
                       .map(person -> new Adult(person))
                       .collect(Collectors.toList());

或者,如果我們想使用特定的實現類來收集結果:

List adultList = persons
                 .stream()
                 .filter(p -> p.getAge() > 18)
                 .map(person -> new Adult(person))
                 .collect(Collectors.toCollection(ArrayList::new));

4.2順序流與並行流
    每個Stream都有兩種模式:順序執行和並行執行。
順序流:

List <Person> people = list.getStream.collect(Collectors.toList());

並行流:

List <Person> people = list.getStream.parallel().collect(Collectors.toList());

    顧名思義,當使用順序方式去遍歷時,每個item讀完后再讀下一個item。而使用並行去遍歷時,數組會被分成多個段,其中每一個都在不同的線程中處理,然后將結果一起輸出。
4.2.1並行流原理:

List originalList = someData;
split1 = originalList(0, mid);//將數據分小部分
split2 = originalList(mid,end);
new Runnable(split1.process());//小部分執行操作
new Runnable(split2.process());
List revisedList = split1 + split2;//將結果合並

    大家對hadoop有稍微了解就知道,里面的 MapReduce 本身就是用於並行處理大數據集的軟件框架,其處理大數據的核心思想就是大而化小,分配到不同機器去運行map,最終通過reduce將所有機器的結果結合起來得到一個最終結果,與MapReduce不同,Stream則是利用多核技術可將大數據通過多核並行處理,而MapReduce則可以分布式的。
4.2.2順序與並行性能測試對比
    如果是多核機器,理論上並行流則會比順序流快上一倍,下面是測試代碼

public static void main(String[] args) {
        long startTime = System.nanoTime();
        int[] array = IntStream.range(0, 1_000_000).filter(p -> p % 2 == 0)
                .toArray();
        System.out.println("串行時間是:" + (System.nanoTime() - startTime));
        startTime = System.nanoTime();
        array = IntStream.range(0, 1_000_000).parallel()
                .filter(p -> p % 2 == 0).toArray();
        System.out.println("並行時間是:" + (System.nanoTime() - startTime));
    }

打印結果:

串行時間是:60908736
並行時間是:18440499

4.3關於Folk/Join框架
    應用硬件的並行性在java 7就有了,那就是 java.util.concurrent 包的新增功能之一是一個 fork-join 風格的並行分解框架,同樣也很強大高效,有興趣的同學去研究,這里不詳談了,相比Stream.parallel()這種方式,我更傾向於后者。

5.總結
    如果沒有lambda,Stream用起來相當別扭,他會產生大量的匿名內部類,比如上面的4.1.2map例子,如果沒有default method,集合框架更改勢必會引起大量的改動,所以lambda+default method使得jdk庫更加強大,以及靈活,Stream以及集合框架的改進便是最好的證明。

四、類型注解
1.概述
    本文將介紹java 8的第二個特性:類型注解。
    注解大家都知道,從java5開始加入這一特性,發展到現在已然是遍地開花,在很多框架中得到了廣泛的使用,用來簡化程序中的配置。那充滿爭議的類型注解究竟是什么?復雜還是便捷?
2. 什么是類型注解
    在java 8之前,注解只能是在聲明的地方所使用,比如類,方法,屬性;java 8里面,注解可以應用在任何地方,比如:

•   創建類實例
        new @Interned MyObject();
•   類型映射
       myString = (@NonNull String) str;
•   implements 語句中
        class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
•   throw exception聲明
        void monitorTemperature() throws @Critical TemperatureException { ... }

    需要注意的是,類型注解只是語法而不是語義,並不會影響java的編譯時間,加載時間,以及運行時間,也就是說,編譯成class文件的時候並不包含類型注解。
3. 類型注解的作用

Collections.emptyList().add("One");
int i=Integer.parseInt("hello");
System.console().readLine();

    上面的代碼編譯是通過的,但運行是會分別報UnsupportedOperationException;NumberFormatException;NullPointerException異常,這些都是runtime error;
    類型注解被用來支持在Java的程序中做強類型檢查。配合插件式的check framework,可以在編譯的時候檢測出runtime error,以提高代碼質量。這就是類型注解的作用了。
4. check framework
    check framework是第三方工具,配合Java的類型注解效果就是1+1>2。它可以嵌入到javac編譯器里面,可以配合ant和maven使用,也可以作為eclipse插件。地址是http://types.cs.washington.edu/checker-framework/
    check framework可以找到類型注解出現的地方並檢查,舉個簡單的例子:

import checkers.nullness.quals.*;
public class GetStarted {
    void sample() {
        @NonNull Object ref = new Object();
    }
}

使用javac編譯上面的類

javac -processor checkers.nullness.NullnessChecker GetStarted.java

編譯是通過,但如果修改成

@NonNull Object ref = null;

再次編譯,則出現

GetStarted.java:5: incompatible types.
found   : @Nullable <nulltype>
required: @NonNull Object
           @NonNull Object ref = null;
                              ^

1 error
    如果你不想使用類型注解檢測出來錯誤,則不需要processor,直接javac GetStarted.java是可以編譯通過的,這是在java 8 with Type Annotation Support版本里面可以,但java 5,6,7版本都不行,因為javac編譯器不知道@NonNull是什么東西,但check framework 有個向下兼容的解決方案,就是將類型注解nonnull用/**/注釋起來
,比如上面例子修改為

import checkers.nullness.quals.*;
public class GetStarted {
    void sample() {
        /*@NonNull*/ Object ref = null;
    }
}

5.關於JSR 308
JSR 308想要解決在Java 1.5注解中出現的兩個問題:
    • 在句法上對注解的限制:只能把注解寫在聲明的地方
    • 類型系統在語義上的限制:類型系統還做不到預防所有的bug
JSR 308 通過如下方法解決上述兩個問題:
    • 對Java語言的句法進行擴充,允許注解出現在更多的位置上。包括:方法接收器(method receivers,譯注:例public int size() @Readonly { ... }),泛型參數,數組,類型轉換,類型測試,對象創建,類型參數綁定,類繼承和throws子句。其實就是類型注解,現在是java 8的一個特性
    • 通過引入可插拔的類型系統(pluggable type systems)能夠創建功能更強大的注解處理器。類型檢查器對帶有類型限定注解的源碼進行分析,一旦發現不匹配等錯誤之處就會產生警告信息。其實就是check framework
對JSR308,有人反對,覺得更復雜更靜態了,比如

@NotEmpty List<@NonNull String> strings = new ArrayList<@NonNull String>()> 

換成動態語言為

var strings = ["one", "two"]; 

有人贊成,說到底,代碼才是“最根本”的文檔。代碼中包含的注解清楚表明了代碼編寫者的意圖。當沒有及時更新或者有遺漏的時候,恰恰是注解中包含的意圖信息,最容易在其他文檔中被丟失。而且將運行時的錯誤轉到編譯階段,不但可以加速開發進程,還可以節省測試時檢查bug的時間。

五、重復注解
1.總結
前面介紹了:
    lambda表達式和默認方法 (JEP 126)
    批量數據操作(JEP 107)
    類型注解(JEP 104)
注:JEP=JDK Enhancement-Proposal (JDK 增強建議 ),每個JEP即一個新特性。
    在java 8里面,注解一共有2個改進,一個是類型注解,在上篇已經介紹了,本篇將介紹另外一個注解的改進:重復注解(JEP 120)。
2.什么是重復注解
    允許在同一申明類型(類,屬性,或方法)的多次使用同一個注解
3.一個簡單的例子
    java 8之前也有重復使用注解的解決方案,但可讀性不是很好,比如下面的代碼:

public @interface Authority {
     String role();
}

public @interface Authorities {
    Authority[] value();
}

public class RepeatAnnotationUseOldVersion {
    @Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
    public void doSomeThing(){
    }
}

    由另一個注解來存儲重復注解,在使用時候,用存儲注解Authorities來擴展重復注解,我們再來看看java 8里面的做法:

@Repeatable(Authorities.class)
public @interface Authority {
     String role();
}
public @interface Authorities {
    Authority[] value();
}
public class RepeatAnnotationUseNewVersion {
    @Authority(role="Admin")
    @Authority(role="Manager")
    public void doSomeThing(){ }
}

    不同的地方是,創建重復注解Authority時,加上@Repeatable,指向存儲注解Authorities,在使用時候,直接可以重復使用Authority注解。從上面例子看出,java 8里面做法更適合常規的思維,可讀性強一點

六、泛型的目標類型推斷
1.簡單理解泛型
    泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。通俗點將就是“類型的變量”。這種類型變量可以用在類、接口和方法的創建中。
    理解Java泛型最簡單的方法是把它看成一種便捷語法,能節省你某些Java類型轉換(casting)上的操作:

List<Apple> box = new ArrayList<Apple>();
box.add(new Apple());
Apple apple =box.get(0);

    上面的代碼自身已表達的很清楚:box是一個裝有Apple對象的List。get方法返回一個Apple對象實例,這個過程不需要進行類型轉換。沒有泛型,上面的代碼需要寫成這樣:

 Apple apple = (Apple)box.get(0);

2. 泛型的尷尬
    泛型的最大優點是提供了程序的類型安全同時可以向后兼容,但也有尷尬的地方,就是每次定義時都要寫明泛型的類型,這樣顯示指定不僅感覺有些冗長,最主要是很多程序員不熟悉泛型,因此很多時候不能夠給出正確的類型參數,現在通過編譯器自動推斷泛型的參數類型,能夠減少這樣的情況,並提高代碼可讀性。
3.java7的泛型類型推斷改進
    在以前的版本中使用泛型類型,需要在聲明並賦值的時候,兩側都加上泛型類型。例如:

Map<String, String> myMap = new HashMap<String, String>();

    你可能覺得:老子在聲明變量的的時候已經指明了參數類型,為毛還要在初始化對象時再指定?幸好,在Java SE 7中,這種方式得以改進,現在你可以使用如下語句進行聲明並賦值:

    Map<String, String> myMap = new HashMap<>(); //注意后面的"<>"

    在這條語句中,編譯器會根據變量聲明時的泛型類型自動推斷出實例化HashMap時的泛型類型。再次提醒一定要注意new HashMap后面的“<>”,只有加上這個“<>”才表示是自動類型推斷,否則就是非泛型類型的HashMap,並且在使用編譯器編譯源代碼時會給出一個警告提示。
    但是:Java SE 7在創建泛型實例時的類型推斷是有限制的:只有構造器的參數化類型在上下文中被顯著的聲明了,才可以使用類型推斷,否則不行。例如:下面的例子在java 7無法正確編譯(但現在在java8里面可以編譯,因為根據方法參數來自動推斷泛型的類型):

List<String> list = new ArrayList<>();
list.add("A");
// 由於addAll期望獲得Collection<? extends String>類型的參數,因此下面的語句無法通過
list.addAll(new ArrayList<>()); 

4 Java8的泛型類型推斷改進
    java8里面泛型的目標類型推斷主要2個:
    1.支持通過方法上下文推斷泛型目標類型
    2.支持在方法調用鏈路當中,泛型類型推斷傳遞到最一個方法
讓我們看看官網的例子

class List<E> {
   static <Z> List<Z> nil() { ... };
   static <Z> List<Z> cons(Z head, List<Z> tail) { ... };
   E head() { ... }
}   

根據JEP101的特性,我們在調用上面方法的時候可以這樣寫

//通過方法賦值的目標參數來自動推斷泛型的類型
List<String> l = List.nil();
//而不是顯示的指定類型
//List<String> l = List.<String>nil();
//通過前面方法參數類型推斷泛型的類型
List.cons(42, List.nil());
//而不是顯示的指定類型
//List.cons(42, List.<Integer>nil());

總結
    以上是JEP101的特性內容了,Java作為靜態語言的代表者,可以說類型系統相當豐富。導致類型間互相轉換的問題困擾着每個java程序員,通過編譯器自動推斷類型的東西可以稍微緩解一下類型轉換太復雜的問題。 雖然說是小進步,但對於我們天天寫代碼的程序員,肯定能帶來巨大的作用,至少心情更愉悅了~~說不定在java 9里面,我們會得到一個通用的類型var,像js或者scala的一些動態語言那樣^_^
七、深入解析日期和時間-JSR310
    日期是商業邏輯計算一個關鍵的部分,任何企業應用程序都需要處理時間問題。應用程序需要知道當前的時間點和下一個時間點,有時它們還必須計算這兩個時間點之間的路徑。但java之前的日期做法太令人惡心了,我們先來吐槽一下
1.吐槽java.util.Date跟Calendar
    Tiago Fernandez做過一次投票,選舉最爛的JAVA API,排第一的EJB2.X,第二的就是日期API。
1.1槽點一
    最開始的時候,Date既要承載日期信息,又要做日期之間的轉換,還要做不同日期格式的顯示,職責較繁雜,后來從JDK 1.1 開始,這三項職責分開了:
    • 使用Calendar類實現日期和時間字段之間轉換;
    • 使用DateFormat類來格式化和分析日期字符串;
    • 而Date只用來承載日期和時間信息。
    原有Date中的相應方法已廢棄。不過,無論是Date,還是Calendar,都用着太不方便了,這是API沒有設計好的地方。
1.2槽點二
    坑爹的year和month

Date date = new Date(2012,1,1);
System.out.println(date);

輸出Thu Feb 01 00:00:00 CST 3912
    觀察輸出結果,year是2012+1900,而month,月份參數我不是給了1嗎?怎么輸出二月(Feb)了?
應該曾有人告訴你,如果你要設置日期,應該使用 java.util.Calendar,像這樣…

Calendar calendar = Calendar.getInstance();
calendar.set(2013, 8, 2);

    這樣寫又不對了,calendar的month也是從0開始的,表達8月份應該用7這個數字,要么就干脆用枚舉

calendar.set(2013, Calendar.AUGUST, 2);

    注意上面的代碼,Calendar年份的傳值不需要減去1900(當然月份的定義和Date還是一樣),這種不一致真是讓人抓狂!
    有些人可能知道,Calendar相關的API是IBM捐出去的,所以才導致不一致。
1.3槽點三
java.util.Datejava.util.Calendar中的所有屬性都是可變的
下面的代碼,計算兩個日期之間的天數….

public static void main(String[] args) {
    Calendar birth = Calendar.getInstance();
    birth.set(1975, Calendar.MAY, 26);
    Calendar now = Calendar.getInstance();
    System.out.println(daysBetween(birth, now));
    System.out.println(daysBetween(birth, now)); // 顯示 0?
 }  

public static long daysBetween(Calendar begin, Calendar end) {
    long daysBetween = 0;
    while(begin.before(end)) {
        begin.add(Calendar.DAY_OF_MONTH, 1);
        daysBetween++;
    }
    return daysBetween;
}

    daysBetween有點問題,如果連續計算兩個Date實例的話,第二次會取得0,因為Calendar狀態是可變的,考慮到重復計算的場合,最好復制一個新的Calendar

public static long daysBetween(Calendar begin, Calendar end) {
    Calendar calendar = (Calendar) begin.clone(); // 復制
    long daysBetween = 0;
    while(calendar.before(end)) {
        calendar.add(Calendar.DAY_OF_MONTH, 1);
        daysBetween++;
    }
    return daysBetween;
}

2.JSR310
    以上種種,導致目前有些第三方的java日期庫誕生,比如廣泛使用的JODA-TIME,還有Date4j等,雖然第三方庫已經足夠強大,好用,但還是有兼容問題的,比如標准的JSF日期轉換器與joda-time API就不兼容,你需要編寫自己的轉換器,所以標准的API還是必須的,於是就有了JSR310。
    JSR 310實際上有兩個日期概念。第一個是Instant,它大致對應於java.util.Date類,因為它代表了一個確定的時間點,即相對於標准Java紀元(1970年1月1日)的偏移量;但與java.util.Date類不同的是其精確到了納秒級別。
    第二個對應於人類自身的觀念,比如LocalDate和LocalTime。他們代表了一般的時區概念,要么是日期(不包含時間),要么是時間(不包含日期),類似於java.sql的表示方式。此外,還有一個MonthDay,它可以存儲某人的生日(不包含年份)。每個類都在內部存儲正確的數據而不是像java.util.Date那樣利用午夜12點來區分日期,利用1970-01-01來表示時間。
    目前Java8已經實現了JSR310的全部內容。新增了java.time包定義的類表示了日期-時間概念的規則,包括instants, durations, dates, times, time-zones and periods。這些都是基於ISO日歷系統,它又是遵循 Gregorian規則的。最重要的一點是值不可變,且線程安全,通過下面一張圖,我們快速看下java.time包下的一些主要的類的值的格式,方便理解。

2.1方法概覽
    該包的API提供了大量相關的方法,這些方法一般有一致的方法前綴:

of:靜態工廠方法。
parse:靜態工廠方法,關注於解析。
get:獲取某些東西的值。
is:檢查某些東西的是否是truewith:不可變的setter等價物。
plus:加一些量到某個對象。
minus:從某個對象減去一些量。
to:轉換到另一個類型。
at:把這個對象與另一個對象組合起來,例如: date.atTime(time)。

2.2 與舊的API對應關系
這里寫圖片描述
3.簡單使用java.time的API

public class TimeIntroduction {
    public static void testClock() throws InterruptedException {
        //時鍾提供給我們用於訪問某個特定時區的瞬時時間、日期 和 時間的。 
//系統默認UTC時鍾(當前瞬時時間 System.currentTimeMillis()) 
        Clock c1 = Clock.systemUTC(); 
          // c1.millis()每次調用將返回當前瞬時時間(UTC) 
        System.out.println(c1.millis()); // 1428040817838

        Clock c2 = Clock.systemDefaultZone(); //系統默認時區時鍾(當前瞬時時間) 
        Clock c31 = Clock.system(ZoneId.of("Europe/Paris")); //巴黎時區 
          System.out.println(c31.millis());  // 1428040817903
        Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));//上海時區 
        System.out.println(c32.millis());//1428040817903

        Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));
//固定上海時區時鍾 
        System.out.println(c4.millis());//1428040817903

        Thread.sleep(1000);
         //不變即時鍾時鍾在那一個點不動
        System.out.println(c4.millis());//1428040817903
        //相對於系統默認時鍾兩秒的時鍾 
Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); 
        System.out.println(c1.millis());//1428041153678
        System.out.println(c5.millis());//1428041155678
    }
    public static void testInstant() {
        //瞬時時間相當於以前的System.currentTimeMillis() 
        Instant instant1 = Instant.now();
//精確到秒 得到相對於1970-01-01 00:00:00 UTC的一個時間 
        System.out.println(instant1.getEpochSecond());//1428041228
        System.out.println(instant1.toEpochMilli()); //精確到毫秒1428041228297
        Clock clock1 = Clock.systemUTC(); //獲取系統UTC默認時鍾 
        Instant instant2 = Instant.now(clock1);//得到時鍾的瞬時時間 
        System.out.println(instant2.toEpochMilli());//1428041228297
        Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); 
//固定瞬時時間時鍾 
        Instant instant3 = Instant.now(clock2);//得到時鍾的瞬時時間 
        System.out.println(instant3.toEpochMilli());//1428041228297
    }
    public static void testLocalDateTime() {
        //使用默認時區時鍾瞬時時間創建 Clock.systemDefaultZone() -->
//即相對於 ZoneId.systemDefault()默認時區 
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);// 2015-04-03T14:09:07.781
//自定義時區 
        LocalDateTime now2 = LocalDateTime.now(ZoneId.of("Europe/Paris"));
        System.out.println(now2);//會以相應的時區顯示日期2015-04-03T08:09:07.781
//自定義時鍾 
        Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));
        LocalDateTime now3 = LocalDateTime.now(clock);
        System.out.println(now3);//會以相應的時區顯示日期2015-04-03T12:09:07.796
//不需要寫什么相對時間 如java.util.Date 年是相對於1900 月是從0開始 
//2013-12-31 23:59 
        LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
//年月日 時分秒 納秒 
        LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59, 59, 11);
//使用瞬時時間 + 時區 
        Instant instant = Instant.now();
        LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
        System.out.println(d3);// 2015-04-03T14:09:07.796
//解析String--->LocalDateTime 
        LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");
        System.out.println(d4);// 2013-12-31T23:59
        LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");
//999毫秒 等價於999000000納秒 
        System.out.println(d5);// 2013-12-31T23:59:59.999
//使用DateTimeFormatter API 解析 和 格式化 
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
      LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);
        System.out.println(formatter.format(d6));// 2013/12/31 23:59:59
//時間獲取 
        System.out.println(d6.getYear());//2013
        System.out.println(d6.getMonth());//DECEMBER
        System.out.println(d6.getDayOfYear());//365
        System.out.println(d6.getDayOfMonth());//31
        System.out.println(d6.getDayOfWeek());//TUESDAY
        System.out.println(d6.getHour());//23
        System.out.println(d6.getMinute());//59
        System.out.println(d6.getSecond());//59
        System.out.println(d6.getNano());//0
//時間增減 
        LocalDateTime d7=d6.minusDays(1);//minusDays減去的天數,plusDays增加的天數
        LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
//LocalDate 即年月日 無時分秒 
//LocalTime即時分秒 無年月日 
//API和LocalDateTime類似就不演示了 
    }
    public static void testZonedDateTime() {
        //即帶有時區的date-time 存儲納秒、時區和時差(避免與本地date-time歧義)。 
//API和LocalDateTime類似,只是多了時差(如
//2013-12-20T10:35:50.711+08:00[Asia/Shanghai]) 
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println(now);// 2015-04-03T14:18:39.701+08:00[Asia/Shanghai]
        ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
        System.out.println(now2);// 2015-04-03T08:18:39.717+02:00[Europe/Paris]
//其他的用法也是類似的 就不介紹了 
        ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");
        System.out.println(z1);// 2013-12-31T23:59:59+01:00[Europe/Paris]
    }
    public static void testDuration() {
        //表示兩個瞬時時間的時間段 
        Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());
//得到相應的時差 
        System.out.println(d1.toDays());//0
        System.out.println(d1.toHours());//3
        System.out.println(d1.toMinutes());//205
        System.out.println(d1.toMillis());//12323123
        System.out.println(d1.toNanos());//12323123000000
//1天時差 類似的還有如ofHours() 
        Duration d2 = Duration.ofDays(1);
        System.out.println(d2.toDays());//1
    }
    public static void testChronology() {
        //提供對java.util.Calendar的替換,提供對年歷系統的支持 
        Chronology c = HijrahChronology.INSTANCE;
        ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
        System.out.println(d);// Hijrah-umalqura AH 1436-06-14T14:22:09.300
    }
    /** * 新舊日期轉換 */
    public static void testNewOldDateConversion(){
        Instant instant=new Date().toInstant();
        Date date=Date.from(instant);
        System.out.println(instant);// 2015-04-03T06:22:55.793Z
        System.out.println(date);// Fri Apr 03 14:22:55 CST 2015
    }
    public static void main(String[] args) throws InterruptedException {
        testClock();
        testInstant();
        testLocalDateTime();
        testZonedDateTime();
        testDuration();
        testChronology();
        testNewOldDateConversion();
    }
}

4.與Joda-Time的區別
    其實JSR310的規范領導者Stephen Colebourne,同時也是Joda-Time的創建者,JSR310是在Joda-Time的基礎上建立的,參考了絕大部分的API,但並不是說JSR310=JODA-Time,下面幾個比較明顯的區別是
    1. 最明顯的變化就是包名(從org.joda.time以及java.time)
    2. JSR310不接受NULL值,Joda-Time視NULL值為0
    3. JSR310的計算機相關的時間(Instant)和與人類相關的時間(DateTime)之間的差別變得更明顯
    4. JSR310所有拋出的異常都是DateTimeException的子類。雖然DateTimeException是一個RuntimeException
5.總結
對比舊的日期API
這里寫圖片描述
    日期與時間處理API,在各種語言中,可能都只是個不起眼的API,如果你沒有較復雜的時間處理需求,可能只是利用日期與時間處理API取得系統時間,簡單做些顯示罷了,然而如果認真看待日期與時間,其復雜程度可能會遠超過你的想象,天文、地理、歷史、政治、文化等因素,都會影響到你對時間的處理。所以在處理時間上,最好選用JSR310(如果你用java8的話就實現310了),或者Joda-Time。
    不止是java面臨時間處理的尷尬,其他語言同樣也遇到過類似的問題,比如

Arrow:Python 中更好的日期與時間處理庫
Moment.js:JavaScript 中的日期庫
Noda-Time:.NET 陣營的 Joda-Time 的復制

八、StampedLock將是解決同步問題的新寵
        Java8就像一個寶藏,一個小的API改進,也足與寫一篇文章,比如同步,一直是多線程並發編程的一個老話題,相信沒有人喜歡同步的代碼,這會降低應用的吞吐量等性能指標,最壞的時候會掛起死機,但是即使這樣你也沒得選擇,因為要保證信息的正確性。所以本文決定將從synchronized、Lock到Java8新增的StampedLock進行對比分析,相信StampedLock不會讓大家失望。
1. synchronized
    在java5之前,實現同步主要是使用synchronized。它是Java語言的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多只有一個線程執行該段代碼。
    有四種不同的同步塊:
    1. 實例方法
    2. 靜態方法
    3. 實例方法中的同步塊
    4. 靜態方法中的同步塊
大家對此應該不陌生,所以不多講了,以下是代碼示例

synchronized(this)
// do operation
}

    小結:在多線程並發編程中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,但是隨着Java SE1.6對Synchronized進行了各種優化之后,性能上也有所提升。
    2. Lock
    它是Java 5在java.util.concurrent.locks新增的一個API。
Lock是一個接口,核心方法是lock(),unlock(),tryLock(),實現類有

ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock;
ReentrantReadWriteLock, ReentrantLock 

    和synchronized鎖都有相同的內存語義。
    與synchronized不同的是,Lock完全用Java寫成,在java這個層面是無關JVM實現的。Lock提供更靈活的鎖機制,很多synchronized 沒有提供的許多特性,比如鎖投票,定時鎖等候和中斷鎖等候,但因為lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中
下面是Lock的一個代碼示例

rwlock.writeLock().lock();
try {
// do operation
} finally {
rwlock.writeLock().unlock();
}

    小結:比synchronized更靈活、更具可伸縮性的鎖定機制,但不管怎么說還是synchronized代碼要更容易書寫些
3.StampedLock
    它是java8在java.util.concurrent.locks新增的一個API。
ReentrantReadWriteLock 在沒有任何讀寫鎖時,才可以取得寫入鎖,這可用於實現了悲觀讀取(Pessimistic Reading),即如果執行中進行讀取時,經常可能有另一執行要寫入的需求,為了保持同步,ReentrantReadWriteLock 的讀取鎖定就可派上用場。
    然而,如果讀取執行情況很多,寫入很少的情況下,使用 ReentrantReadWriteLock 可能會使寫入線程遭遇飢餓(Starvation)問題,也就是寫入線程遲遲無法競爭到鎖定而一直處於等待狀態。
    StampedLock控制鎖有三種模式(寫,讀,樂觀讀),一個StampedLock狀態是由版本和模式兩個部分組成,鎖獲取方法返回一個數字作為票據stamp,它用相應的鎖狀態表示並控制訪問,數字0表示沒有寫鎖被授權訪問。在讀鎖上分為悲觀鎖和樂觀鎖。
    所謂的樂觀讀模式,也就是若讀的操作很多,寫的操作很少的情況下,你可以樂觀地認為,寫入與讀取同時發生幾率很少,因此不悲觀地使用完全的讀取鎖定,程序可以查看讀取資料之后,是否遭到寫入執行的變更,再采取后續的措施(重新讀取變更信息,或者拋出異常) ,這一個小小改進,可大幅度提高程序的吞吐量!!
    下面是java doc提供的StampedLock一個例子

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看樂觀讀鎖案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //獲得一個樂觀讀鎖
     double currentX = x, currentY = y; //將兩個字段讀入本地局部變量
     if (!sl.validate(stamp)) { //檢查發出樂觀讀鎖后同時是否有其他寫鎖發生?
        stamp = sl.readLock(); //如果沒有,我們再次獲得一個讀悲觀鎖
        try {
          currentX = x; // 將兩個字段讀入本地局部變量
          currentY = y; // 將兩個字段讀入本地局部變量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
//下面是悲觀讀鎖案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循環,檢查當前狀態是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉為寫鎖
         if (ws != 0L) { //這是確認轉為寫鎖是否成功
           stamp = ws; //如果成功 替換票據
           x = newX; //進行狀態改變
           y = newY; //進行狀態改變
           break;
         }else { //如果不能成功轉換為寫鎖
           sl.unlockRead(stamp); //我們顯式釋放讀鎖
           stamp = sl.writeLock(); //顯式直接進行寫鎖 然后再通過循環再試
         }
       }
     } finally {
       sl.unlock(stamp); //釋放讀鎖或寫鎖
     }
   }
 }

小結:
    StampedLock要比ReentrantReadWriteLock更加廉價,也就是消耗比較小。
4. StampedLock與ReadWriteLock性能對比
    下圖是和ReadWritLock相比,在一個線程情況下,是讀速度其4倍左右,寫是1倍。
這里寫圖片描述
下圖是六個線程情況下,讀性能是其幾十倍,寫性能也是近10倍左右:
這里寫圖片描述
下圖是吞吐量提高:
這里寫圖片描述
5.總結
    5.1、synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定;
    5.2、ReentrantLock、ReentrantReadWriteLock,、StampedLock都是對象層面的鎖定,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中;
    5.3、StampedLock 對吞吐量有巨大的改進,特別是在讀線程越來越多的場景下;
    5.4、StampedLock有一個復雜的API,對於加鎖操作,很容易誤用其他方法;
    5.5、當只有少量競爭者的時候,synchronized是一個很好的通用的鎖實現;
    5.6、當線程增長能夠預估,ReentrantLock是一個很好的通用的鎖實現;
    StampedLock 可以說是Lock的一個很好的補充,吞吐量以及性能上的提升足以打動很多人了,但並不是說要替代之前Lock的東西,畢竟他還是有些應用場景的,起碼API比StampedLock容易入手.
九、Base64詳解
    BASE64 編碼是一種常用的字符編碼,在很多地方都會用到。但base64不是安全領域下的加密解密算法。能起到安全作用的效果很差,而且很容易破解,他核心作用應該是傳輸數據的正確性,有些網關或系統只能使用ASCII字符。Base64就是用來將非ASCII字符的數據轉換成ASCII字符的一種方法,而且base64特別適合在http,mime協議下快速傳輸數據。
    1. JDK里面實現Base64的API
    在JDK1.6之前,JDK核心類一直沒有Base64的實現類,有人建議用Sun/Oracle JDK里面的sun.misc.BASE64Encoder 和 sun.misc.BASE64Decoder,使用它們的優點就是不需要依賴第三方類庫,缺點就是可能在未來版本會被刪除(用maven編譯會發出警告),而且性能不佳,后面會有性能測試。
    JDK1.6中添加了另一個Base64的實現,javax.xml.bind.DatatypeConverter兩個靜態方法parseBase64Binary 和 printBase64Binary,隱藏在javax.xml.bind包下面,不被很多開發者知道。
在Java 8在java.util包下面實現了BASE64編解碼API,而且性能不俗,API也簡單易懂,下面展示下這個類的使用例子。
2. java.util.Base64
    該類提供了一套靜態方法獲取下面三種BASE64編解碼器:
1)Basic編碼:是標准的BASE64編碼,用於處理常規的需求

// 編碼
String asB64 = Base64.getEncoder()
.encodeToString("some string".getBytes("utf-8"));
System.out.println(asB64); // 輸出為: c29tZSBzdHJpbmc=
// 解碼
byte[] asBytes = Base64.getDecoder().decode("c29tZSBzdHJpbmc=");
System.out.println(new String(asBytes, "utf-8")); // 輸出為: some string

2)URL編碼:使用下划線替換URL里面的反斜線“/”

String urlEncoded = Base64.getUrlEncoder()
.encodeToString("subjects?abcd".getBytes("utf-8"));
System.out.println("Using URL Alphabet: " + urlEncoded);
// 輸出為:
Using URL Alphabet: c3ViamVjdHM_YWJjZA==

3)MIME編碼:使用基本的字母數字產生BASE64輸出,而且對MIME格式友好:每一行輸出不超過76個字符,而且每行以“\r\n”符結束。

StringBuilder sb = new StringBuilder();
for (int t = 0; t < 10; ++t) {
  sb.append(UUID.randomUUID().toString());
}
byte[] toEncode = sb.toString().getBytes("utf-8");
String mimeEncoded = Base64.getMimeEncoder().encodeToString(toEncode);
System.out.println(mimeEncoded);

2.總結
    如果你需要一個性能好,可靠的Base64編解碼器,不要找JDK外面的了,java8里面的java.util.Base64以及java6中隱藏很深的javax.xml.bind.DatatypeConverter,他們兩個都是不錯的選擇。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com