天天看點

Java5-Java8新特性,這回全了

Java5:

泛型 Generics

引用泛型之後,允許指定集合裡元素的類型,免去了強制類型轉換,并且能在編譯時刻進行類型檢查的好處。Parameterized Type作為參數和傳回值,Generic是vararg、annotation、enumeration、collection的基石。

   類型安全:抛棄List、Map,使用List<T>、Map<K,V>給它們添加元素或者使用Iterator<T>周遊時,編譯期就可以給你檢查出類型錯誤
   方法參數和傳回值加上了Type,抛棄List、Map,使用List<T>、Map<K,V>
   不需要類型轉換List<String> list=new ArrayList<String>();String str=list.get(i);
   類型通配符“?”:假設一個列印List<T>中元素的方法printList,我們希望任何類型T的List<T>都可以被列印: 
public void printList(List<?> list,PrintStream out)throws IOException{
for(Iterator<?> i=list.iterator();i.hasNext();){
System.out.println(i.next.toString());
}
}
如果通配符?讓我們的參數類型過于廣泛,我們可以把List<?>、Iterator<?> 修改為:
List<? Extends Number>、Iterator<? Extends Number>限制一下它。
           

2、枚舉類型 Enumeration

3、自動裝箱拆箱

(自動類型包裝和解包)autoboxing & unboxing: (int ——Integer)。

4、可變參數

varargs參數類型相同時,把重載函數合并到一起了。

如:public void test(Object … objs){

for(Object obj:objs){

System.out.println(obj);

}

}

5、Annotations

它是java中的metadata

Tiger中預定義的三種标準annotation

a 、Override

指出某個method覆寫了superclass 的method當你要覆寫的方法名拼寫錯時編譯不通過

b、Deprecated

指出某個method或element類型的使用是被阻止的,子類将不能覆寫該方法

c、SupressWarnings

關閉class、method、field、variable 初始化的編譯期警告,比如:List沒有使用 Generic,則@SuppressWarnings(“unchecked”)去掉編譯期警告。

自定義annotation

public @interface Marked{}

meta-annotation

或者說annotation的annotation,四種标準的meta-annotation全部定義在java.lang.annotaion包中:

a, Target:指定所定義的annotation可以用在哪些程式單元上,如果Target沒有指定,則表示該annotation可以使用在任意程式單元上

@Target({ElementType.ANNOTATION_TYPE,  
  ElementType.CONSTRUCTOR,  
  ElementType.FIELD,  
  ElementType.LOCAL_VARIABLE,  
  ElementType.METHOD,  
  ElementType.PACKAGE,  
  ElementType.PARAMETER,  
  ElementType.TYPE})  
  public @interface TODO {}  
           

b, Retention:指出Java編譯期如何對待annotation,annotation可以被編譯期丢掉,或者保留在編譯過的class檔案中,在annotation被保留時,它也指定是否會在JVM加載class時讀取該annotation

@Retention(RetentionPolicy.SOURCE)  // Annotation會被編譯期丢棄  
public @interface TODO1 {}  
@Retention(RetentionPolicy.CLASS)   // Annotation保留在class檔案中,但會被JVM忽略  
public @interface TODO2 {}  
@Retention(RetentionPolicy.RUNTIME) // Annotation保留在class檔案中且會被JVM讀取  
public @interface TODO3 {}  
           

c, Documented

指出被定義的annotation被視為所熟悉的程式單元的公開API之一,被@Documented标注的annotation會在javadoc中顯示,這在annotation對它标注的元素被用戶端使用時有影響時起作用

d, Inherited

該meta-annotation應用于目标為class的annotation類型上,被此annotattion标注的class會自動繼承父類的annotation

Annotation的反射

我們發現java.lang.Class有許多與Annotation的反射相關的方法,如getAnnotations、isAnnotationpresent,我們可以利用Annotation反射來做許多事情,比如自定義Annotation來做Model對象驗證

@Retention(RetentionPolicy.RUNTIME)  
@Target({ ElementType.FIELD, ElementType.METHOD })  
public @interface RejectEmpty {  
        /** hint title used in error message */  
       String value() default "";  
}  
@Retention(RetentionPolicy.RUNTIME)  
@Target( { ElementType.FIELD, ElementType.METHOD })  
public @interface AcceptInt {  
int min() default Integer.MIN_VALUE;  
int max() default Integer.MAX_VALUE;  
String hint() default "";  
 }  
           

使用@RejectEmpty和@AcceptInt标注我們的Model的field,然後利用反射來做Model驗證

6、foreach語句

(for(int n:numbers))

7、靜态導入

(import static )

8、新的格式化方法

(java.util.Formatter)

formatter.format(“Remaining account balance: $%.2f”, balance);

9、新的線程模型和并發庫Thread Framework

HashMap的替代者ConcurrentHashMap和ArrayList的替代者CopyOnWriteArrayList,在大并發量讀取時采用java.util.concurrent包裡的一些類會讓大家滿意BlockingQueue、Callable、Executor、Semaphore…

Java6

1、引入了一個支援腳本引擎的新架構

2、UI的增強

3、對WebService支援的增強(JAX-WS2.0和JAXB2.0)

4、一系列新的安全相關的增強

5、JDBC4.0

6、Compiler API

7、通用的Annotations支援

Servlet 3.0 新特性概述

Servlet 3.0 作為 Java EE 6 規範體系中一員,随着 Java EE 6 規範一起釋出。該版本在前一版本(Servlet 2.5)的基礎上提供了若幹新特性用于簡化 Web 應用的開發和部署。其中有幾項特性的引入讓開發者感到非常興奮,同時也獲得了 Java 社群的一片贊譽之聲:

1. 異步處理支援:有了該特性,Servlet 線程不再需要一直阻塞,直到業務處理完畢才能再輸出響應,最後才結束該 Servlet 線程。在接收到請求之後,Servlet 線程可以将耗時的操作委派給另一個線程來完成,自己在不生成響應的情況下傳回至容器。針對業務處理較耗時的情況,這将大大減少伺服器資源的占用,并且提高并發處理速度。

2. 新增的注解支援:該版本新增了若幹注解,用于簡化 Servlet、過濾器(Filter)和監聽器(Listener)的聲明,這使得 web.xml 部署描述檔案從該版本開始不再是必選的了。

3. 可插性支援:熟悉 Struts2 的開發者一定會對其通過插件的方式與包括 Spring 在内的各種常用架構的整合特性記憶猶新。将相應的插件封裝成 JAR 包并放在類路徑下,Struts2 運作時便能自動加載這些插件。現在 Servlet 3.0 提供了類似的特性,開發者可以通過插件的方式很友善的擴充已有 Web 應用的功能,而不需要修改原有的應用。

異步處理支援

Servlet 3.0 之前,一個普通 Servlet 的主要工作流程大緻如下:首先,Servlet 接收到請求之後,可能需要對請求攜帶的資料進行一些預處理;接着,調用業務接口的某些方法,以完成業務處理;最後,根據處理的結果送出響應,Servlet 線程結束。其中第二步的業務處理通常是最耗時的,這主要展現在資料庫操作,以及其它的跨網絡調用等,在此過程中,Servlet 線程一直處于阻塞狀态,直到業務方法執行完畢。在處理業務的過程中,Servlet 資源一直被占用而得不到釋放,對于并發較大的應用,這有可能造成性能的瓶頸。對此,在以前通常是采用私有解決方案來提前結束 Servlet 線程,并及時釋放資源。

Servlet 3.0 針對這個問題做了開創性的工作,現在通過使用 Servlet 3.0 的異步處理支援,之前的 Servlet 處理流程可以調整為如下的過程:首先,Servlet 接收到請求之後,可能首先需要對請求攜帶的資料進行一些預處理;接着,Servlet 線程将請求轉交給一個異步線程來執行業務處理,線程本身傳回至容器,此時 Servlet 還沒有生成響應資料,異步線程處理完業務以後,可以直接生成響應資料(異步線程擁有 ServletRequest 和 ServletResponse 對象的引用),或者将請求繼續轉發給其它 Servlet。如此一來, Servlet 線程不再是一直處于阻塞狀态以等待業務邏輯的處理,而是啟動異步線程之後可以立即傳回。

異步處理特性可以應用于 Servlet 和過濾器兩種元件,由于異步處理的工作模式和普通工作模式在實作上有着本質的差別,是以預設情況下,Servlet 和過濾器并沒有開啟異步處理特性,如果希望使用該特性,則必須按照如下的方式啟用:

1. 對于使用傳統的部署描述檔案 (web.xml) 配置 Servlet 和過濾器的情況,Servlet 3.0 為 和 标簽增加了 子标簽,該标簽的預設取值為 false,要啟用異步處理支援,則将其設為 true 即可。以 Servlet 為例,其配置方式如下所示:

<servlet> 
    <servlet-name>DemoServlet</servlet-name> 
    <servlet-class>footmark.servlet.Demo Servlet</servlet-class> 
    <async-supported>true</async-supported> 
</servlet>
           
  1. 對于使用 Servlet 3.0 提供的 @WebServlet 和 @WebFilter 進行 Servlet 或過濾器配置的情況,這兩個注解都提供了 asyncSupported 屬性,預設該屬性的取值為 false,要啟用異步處理支援,隻需将該屬性設定為 true 即可。以 @WebFilter 為例,其配置方式如下所示:

    @WebFilter(urlPatterns = “/demo”,asyncSupported = true)

    public class DemoFilter implements Filter{…}

    一個簡單的模拟異步處理的 Servlet 示例如下:

@WebServlet(urlPatterns = "/demo", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws IOException, ServletException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("進入Servlet的時間:" + new Date() + ".");
        out.flush();

        //在子線程中執行業務調用,并由其負責輸出響應,主線程退出
        AsyncContext ctx = req.startAsync();
        new Thread(new Executor(ctx)).start();

        out.println("結束Servlet的時間:" + new Date() + ".");
        out.flush();
    }
}

public class Executor implements Runnable {
    private AsyncContext ctx = null;
    public Executor(AsyncContext ctx){
        this.ctx = ctx;
    }

    public void run(){
        try {
            //等待十秒鐘,以模拟業務方法的執行
            Thread.sleep();
            PrintWriter out = ctx.getResponse().getWriter();
            out.println("業務處理完畢的時間:" + new Date() + ".");
            out.flush();
            ctx.complete();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           

除此之外,Servlet 3.0 還為異步處理提供了一個監聽器,使用 AsyncListener 接口表示。它可以監控如下四種事件:

1. 異步線程開始時,調用 AsyncListener 的 onStartAsync(AsyncEvent event) 方法;

2. 異步線程出錯時,調用 AsyncListener 的 onError(AsyncEvent event) 方法;

3. 異步線程執行逾時,則調用 AsyncListener 的 onTimeout(AsyncEvent event) 方法;

4. 異步執行完畢時,調用 AsyncListener 的 onComplete(AsyncEvent event) 方法;

要注冊一個 AsyncListener,隻需将準備好的 AsyncListener 對象傳遞給 AsyncContext 對象的 addListener() 方法即可,如下所示:

AsyncContext ctx = req.startAsync(); 
ctx.addListener(new AsyncListener() { 
    public void onComplete(AsyncEvent asyncEvent) throws IOException { 
        // 做一些清理工作或者其他
    } 
    ... 
});
           

新增的注解支援

Servlet 3.0 的部署描述檔案 web.xml 的頂層标簽 有一個 metadata-complete 屬性,該屬性指定目前的部署描述檔案是否是完全的。如果設定為 true,則容器在部署時将隻依賴部署描述檔案,忽略所有的注解(同時也會跳過 web-fragment.xml 的掃描,亦即禁用可插性支援,具體請看後文關于 可插性支援的講解);如果不配置該屬性,或者将其設定為 false,則表示啟用注解支援(和可插性支援)。

@WebServlet

@WebServlet 用于将一個類聲明為 Servlet,該注解将會在部署時被容器處理,容器将根據具體的屬性配置将相應的類部署為 Servlet。該注解具有下表給出的一些常用屬性(以下所有屬性均為可選屬性,但是 vlaue 或者 urlPatterns 通常是必需的,且二者不能共存,如果同時指定,通常是忽略 value 的取值):

表 1. @WebServlet 主要屬性清單

屬性名 類型 描述

name String 指定 Servlet 的 name 屬性,等價于 。如果沒有顯式指定,則該 Servlet 的取值即為類的全限定名。

value String[] 該屬性等價于 urlPatterns 屬性。兩個屬性不能同時使用。

urlPatterns String[] 指定一組 Servlet 的 URL 比對模式。等價于 标簽。

loadOnStartup int 指定 Servlet 的加載順序,等價于 标簽。

initParams WebInitParam[] 指定一組 Servlet 初始化參數,等價于 标簽。

asyncSupported boolean 聲明 Servlet 是否支援異步操作模式,等價于 标簽。

description String 該 Servlet 的描述資訊,等價于 标簽。

displayName String 該 Servlet 的顯示名,通常配合工具使用,等價于 标簽。

下面是一個簡單的示例:

@WebServlet(urlPatterns = {"/simple"}, asyncSupported = true, 
loadOnStartup = -, name = "SimpleServlet", displayName = "ss", 
initParams = {@WebInitParam(name = "username", value = "tom")} 
) 
public class SimpleServlet extends HttpServlet{ … }
           

如此配置之後,就可以不必在 web.xml 中配置相應的 和 元素了,容器會在部署時根據指定的屬性将該類釋出為 Servlet。它的等價的 web.xml 配置形式如下:

<servlet>
    <display-name>ss</display-name>
    <servlet-name>SimpleServlet</servlet-name>
    <servlet-class>footmark.servlet.SimpleServlet</servlet-class>
    <load-on-startup>-</load-on-startup>
    <async-supported>true</async-supported>
    <init-param>
        <param-name>username</param-name>
        <param-value>tom</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>SimpleServlet</servlet-name>
    <url-pattern>/simple</url-pattern>
</servlet-mapping>
           

@WebInitParam

該注解通常不單獨使用,而是配合 @WebServlet 或者 @WebFilter 使用。它的作用是為 Servlet 或者過濾器指定初始化參數,這等價于 web.xml 中 和 的 子标簽。@WebInitParam 具有下表給出的一些常用屬性:

表 2. @WebInitParam 的常用屬性

屬性名 類型 是否可選 描述

name String 否 指定參數的名字,等價于 。

value String 否 指定參數的值,等價于 。

description String 是 關于參數的描述,等價于 。

@WebFilter

@WebFilter 用于将一個類聲明為過濾器,該注解将會在部署時被容器處理,容器将根據具體的屬性配置将相應的類部署為過濾器。該注解具有下表給出的一些常用屬性 ( 以下所有屬性均為可選屬性,但是 value、urlPatterns、servletNames 三者必需至少包含一個,且 value 和 urlPatterns 不能共存,如果同時指定,通常忽略 value 的取值 ):

表 3. @WebFilter 的常用屬性

屬性名 類型 描述

filterName String 指定過濾器的 name 屬性,等價于

value String[] 該屬性等價于 urlPatterns 屬性。但是兩者不應該同時使用。

urlPatterns String[] 指定一組過濾器的 URL 比對模式。等價于 标簽。

servletNames String[] 指定過濾器将應用于哪些 Servlet。取值是 @WebServlet 中的 name 屬性的取值,或者是 web.xml 中 的取值。

dispatcherTypes DispatcherType 指定過濾器的轉發模式。具體取值包括:

ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。

initParams WebInitParam[] 指定一組過濾器初始化參數,等價于 标簽。

asyncSupported boolean 聲明過濾器是否支援異步操作模式,等價于 标簽。

description String 該過濾器的描述資訊,等價于 标簽。

displayName String 該過濾器的顯示名,通常配合工具使用,等價于 标簽。

下面是一個簡單的示例:

@WebFilter(servletNames = {“SimpleServlet”},filterName=”SimpleFilter”)

public class LessThanSixFilter implements Filter{…}

如此配置之後,就可以不必在 web.xml 中配置相應的 和 元素了,容器會在部署時根據指定的屬性将該類釋出為過濾器。它等價的 web.xml 中的配置形式為:

<filter> 
    <filter-name>SimpleFilter</filter-name> 
    <filter-class>xxx</filter-class> 
</filter> 
<filter-mapping> 
    <filter-name>SimpleFilter</filter-name> 
    <servlet-name>SimpleServlet</servlet-name> 
</filter-mapping>
           

@WebListener

該注解用于将類聲明為監聽器,被 @WebListener 标注的類必須實作以下至少一個接口:

•   ServletContextListener
•   ServletContextAttributeListener
•   ServletRequestListener
•   ServletRequestAttributeListener
•   HttpSessionListener
•   HttpSessionAttributeListener
           

該注解使用非常簡單,其屬性如下:

表 4. @WebListener 的常用屬性

屬性名 類型 是否可選 描述

value String 是 該監聽器的描述資訊。

一個簡單示例如下:

@WebListener("This is only a demo listener") 
public class SimpleListener implements ServletContextListener{...}
           

如此,則不需要在 web.xml 中配置 标簽了。它等價的 web.xml 中的配置形式如下:

<listener> 
    <listener-class>footmark.servlet.SimpleListener</listener-class> 
</listener>
           

@MultipartConfig

該注解主要是為了輔助 Servlet 3.0 中 HttpServletRequest 提供的對上傳檔案的支援。該注解标注在 Servlet 上面,以表示該 Servlet 希望處理的請求的 MIME 類型是 multipart/form-data。另外,它還提供了若幹屬性用于簡化對上傳檔案的處理。具體如下:

表 5. @MultipartConfig 的常用屬性

屬性名 類型 是否可選 描述

fileSizeThreshold int 是 當資料量大于該值時,内容将被寫入檔案。

location String 是 存放生成的檔案位址。

maxFileSize long 是 允許上傳的檔案最大值。預設值為 -1,表示沒有限制。

maxRequestSize long 是 針對該 multipart/form-data 請求的最大數量,預設值為 -1,表示沒有限制。

可插性支援

如果說 3.0 版本新增的注解支援是為了簡化 Servlet/ 過濾器 / 監聽器的聲明,進而使得 web.xml 變為可選配置, 那麼新增的可插性 (pluggability) 支援則将 Servlet 配置的靈活性提升到了新的高度。熟悉 Struts2 的開發者都知道,Struts2 通過插件的形式提供了對包括 Spring 在内的各種開發架構的支援,開發者甚至可以自己為 Struts2 開發插件,而 Servlet 的可插性支援正是基于這樣的理念而産生的。使用該特性,現在我們可以在不修改已有 Web 應用的前提下,隻需将按照一定格式打成的 JAR 包放到 WEB-INF/lib 目錄下,即可實作新功能的擴充,不需要額外的配置。

Servlet 3.0 引入了稱之為“Web 子產品部署描述符片段”的 web-fragment.xml 部署描述檔案,該檔案必須存放在 JAR 檔案的 META-INF 目錄下,該部署描述檔案可以包含一切可以在 web.xml 中定義的内容。JAR 包通常放在 WEB-INF/lib 目錄下,除此之外,所有該子產品使用的資源,包括 class 檔案、配置檔案等,隻需要能夠被容器的類加載器鍊加載的路徑上,比如 classes 目錄等。

現在,為一個 Web 應用增加一個 Servlet 配置有如下三種方式 ( 過濾器、監聽器與 Servlet 三者的配置都是等價的,故在此以 Servlet 配置為例進行講述,過濾器和監聽器具有與之非常類似的特性 ):

• 編寫一個類繼承自 HttpServlet,将該類放在 classes 目錄下的對應包結構中,修改 web.xml,在其中增加一個 Servlet 聲明。這是最原始的方式;

• 編寫一個類繼承自 HttpServlet,并且在該類上使用 @WebServlet 注解将該類聲明為 Servlet,将該類放在 classes 目錄下的對應包結構中,無需修改 web.xml 檔案。

• 編寫一個類繼承自 HttpServlet,将該類打成 JAR 包,并且在 JAR 包的 META-INF 目錄下放置一個 web-fragment.xml 檔案,該檔案中聲明了相應的 Servlet 配置。web-fragment.xml 檔案示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-fragment 
    xmlns=http://java.sun.com/xml/ns/javaee
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd"
    metadata-complete="true">
    <servlet>
        <servlet-name>fragment</servlet-name>
        <servlet-class>footmark.servlet.FragmentServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>fragment</servlet-name>
        <url-pattern>/fragment</url-pattern>
    </servlet-mapping>
</web-fragment>
           

從上面的示例可以看出,web-fragment.xml 與 web.xml 除了在頭部聲明的 XSD 引用不同之外,其主體配置與 web.xml 是完全一緻的。

由于一個 Web 應用中可以出現多個 web-fragment.xml 聲明檔案,加上一個 web.xml 檔案,加載順序問題便成了不得不面對的問題。Servlet 規範的專家組在設計的時候已經考慮到了這個問題,并定義了加載順序的規則。

web-fragment.xml 包含了兩個可選的頂層标簽, 和 ,如果希望為目前的檔案指定明确的加載順序,通常需要使用這兩個标簽, 主要用于辨別目前的檔案,而 則用于指定先後順序。一個簡單的示例如下:

<web-fragment...>
    <name>FragmentA</name>
    <ordering>
        <after>
            <name>FragmentB</name>
            <name>FragmentC</name>
        </after>
    <before>
        <others/>
    </before>
    </ordering>
    ...
</web-fragment>
           

如上所示,

<name>

标簽的取值通常是被其它 web-fragment.xml 檔案在定義先後順序時引用的,在目前檔案中一般用不着,它起着辨別目前檔案的作用。

<ordering>

标簽内部,我們可以定義目前 web-fragment.xml 檔案與其他檔案的相對位置關系,這主要通過

<ordering> 的 <after> 和 <before> 子标簽來實作的。在這兩個子标簽内部可以通過 <name>

标簽來指定相對應的檔案。比如:

<after> 
    <name>FragmentB</name> 
    <name>FragmentC</name> 
</after>
           

以上片段則表示目前檔案必須在 FragmentB 和 FragmentC 之後解析。 的使用于此相同,它所表示的是目前檔案必須早于

<before>

标簽裡所列出的 web-fragment.xml 檔案。

除了将所比較的檔案通過

<name> 在 <after> 和 <begin> 中列出之外,Servlet 還提供了一個簡化的标簽 <others/>。它表示除了目前檔案之外的其他所有的 web-fragment.xml 檔案。該标簽的優先級要低于使用 <name> 明确指定的相對位置關系

ServletContext 的性能增強

除了以上的新特性之外,ServletContext 對象的功能在新版本中也得到了增強。現在,該對象支援在運作時動态部署 Servlet、過濾器、監聽器,以及為 Servlet 和過濾器增加 URL 映射等。以 Servlet 為例,過濾器與監聽器與之類似。ServletContext 為動态配置 Servlet 增加了如下方法:

•   ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
•   ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
•   ServletRegistration.Dynamic addServlet(String servletName, String className)
•   <T extends Servlet> T createServlet(Class<T> clazz)
•   ServletRegistration getServletRegistration(String servletName)
•   Map<String,? extends ServletRegistration> getServletRegistrations()
           

其中前三個方法的作用是相同的,隻是參數類型不同而已;通過 createServlet() 方法建立的 Servlet,通常需要做一些自定義的配置,然後使用 addServlet() 方法來将其動态注冊為一個可以用于服務的 Servlet。兩個 getServletRegistration() 方法主要用于動态為 Servlet 增加映射資訊,這等價于在 web.xml( 抑或 web-fragment.xml) 中使用

<servlet-mapping>

标簽為存在的 Servlet 增加映射資訊。

以上 ServletContext 新增的方法要麼是在 ServletContextListener 的 contexInitialized 方法中調用,要麼是在 ServletContainerInitializer 的 onStartup() 方法中調用。

ServletContainerInitializer 也是 Servlet 3.0 新增的一個接口,容器在啟動時使用 JAR 服務 API(JAR Service API) 來發現 ServletContainerInitializer 的實作類,并且容器将 WEB-INF/lib 目錄下 JAR 包中的類都交給該類的 onStartup() 方法處理,我們通常需要在該實作類上使用 @HandlesTypes 注解來指定希望被處理的類,過濾掉不希望給 onStartup() 處理的類。

HttpServletRequest 對檔案上傳的支援

此前,對于處理上傳檔案的操作一直是讓開發者頭疼的問題,因為 Servlet 本身沒有對此提供直接的支援,需要使用第三方架構來實作,而且使用起來也不夠簡單。如今這都成為了曆史,Servlet 3.0 已經提供了這個功能,而且使用也非常簡單。為此,HttpServletRequest 提供了兩個方法用于從請求中解析出上傳的檔案:

•   Part getPart(String name)
•   Collection<Part> getParts()
           

前者用于擷取請求中給定 name 的檔案,後者用于擷取所有的檔案。每一個檔案用一個 javax.servlet.http.Part 對象來表示。該接口提供了處理檔案的簡易方法,比如 write()、delete() 等。至此,結合 HttpServletRequest 和 Part 來儲存上傳的檔案變得非常簡單,如下所示:

Part photo = request.getPart("photo"); 
photo.write("/tmp/photo.jpg"); 
// 可以将兩行代碼簡化為 request.getPart("photo").write("/tmp/photo.jpg") 一行。
           

另外,開發者可以配合前面提到的 @MultipartConfig 注解來對上傳操作進行一些自定義的配置,比如限制上傳檔案的大小,以及儲存檔案的路徑等。其用法非常簡單,故不在此贅述了。

需要注意的是,如果請求的 MIME 類型不是 multipart/form-data,則不能使用上面的兩個方法,否則将抛異常。

總結

Servlet 3.0 的衆多新特性使得 Servlet 開發變得更加簡單,尤其是異步處理特性和可插性支援的出現,必将對現有的 MVC 架構産生深遠影響。雖然我們通常不會自己去用 Servlet 編寫控制層代碼,但是也許在下一個版本的 Struts 中,您就能切實感受到這些新特性帶來的實質性改變。

Java7新特性

一、自動資源管理

早在7.x版本之前,某些可回收資源比如:I/O連結、DB連接配接、TCP/UDP連接配接。開發人員都需要在使用後對其進行手動關閉,如果不關閉或者忘記關閉這些資源,就會長期霸占JVM内部的資源,極大程度上影響了JVM的資源配置設定。就像記憶體管理一樣,開發人員夢寐以求的就是希望有一天再也無需關注繁瑣的資源管理(資源建立、資源就緒、資源回收)。值得慶幸的是7.x為我們帶來了一次徹頭徹尾改變,我們将再也不必以手動管理我們的資源。

早在Java5.x的時候,Java API為開發人員提供了一個Closeable接口。該接口中包含一個close()方法,允許所有可回收資源的類型對其進行重寫實作。7.x版本中幾乎所有的資源類型都實作了Closeable接口,并重寫了close()方法。也就是說所有可回收的系統資源,我們将再不必每次使用完後調用close()方法進行資源回收,這一切全部交接給自動資料總管去做即可。

例如Reader資源類型繼承Closeable接口實作資源自動管理:

public abstract class Reader implements Readable, Closeable

當然如果你需要在程式中使用自動資源管理,還需要使用API提供的新文法支援,這類文法包含在try語句塊内部。看到這裡你可能不禁感歎,try也能支援表達式了,是的7.x确實允許try使用表達式的文法方式實作自動資源管理,但僅限于資源類型。

try(BufferedReader reader = new BufferedReader(new FileReader("filepath here"));)  {  
}  
catch(Exception e)  {  
    e.printStackTrace();  
}  
           

Java7中在try語句中申請資源,實作資源的自動釋放(資源類必須實作java.lang.AutoCloseable接口,一般的檔案、資料庫連接配接等均已實作該接口,close方法将被自動調用)。

public void read(String filename) throws IOException {  
       try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {  
           StringBuilder builder = new StringBuilder();  
  String line = null;  
  while((line=reader.readLine())!=null){  
      builder.append(line);  
      builder.append(String.format("%n"));  
  }  
  return builder.toString();  
      }   
  }  
           

try子句中可以管理多個資源:

public void copyFile(String fromPath, String toPath) throws IOException {  
       try ( InputStream input = new FileInputStream(fromPath);  
      OutputStream output = new FileOutputStream(toPath) ) {  
           byte[] buffer = new byte[];  
  int len = -;  
  while( (len=input.read(buffer))!=- ) {  
      output.write(buffer, , len);  
  }  
       }   
  }  
           

二、“<>”類型推斷運算符

Java5.x新增了許多新的功能,在這些新引入的功能中,泛型最為重要。泛型是一種新的文法元素,泛型的出現導緻整個Java API都發生了變化(比如:Java集合架構就使用了泛型文法)。

在泛型沒有出現之前,我們都是将Object類作為通用的任意資料類型使用。因為在Java語言中,Object類是所有類的超類。但是使用Object類作為任意資料類型并不是安全的,因為在很多時候我們需要将Object類型向下轉換,在這些轉換過程中偶爾也可能出現不比對的類型轉換錯誤。泛型的出現則很好的解決了Object類型所存在的安全性問題,且泛型還擴充了代碼的重用性。

泛型的核心概念就是參數化類型,所謂參數化類型指的就是開發人員可以在外部指定的資料類型來建立泛型類、泛型接口和泛型方法。

List<String> list = new ArrayList<String>();  
           

通過上述程式示例我們可以看出,筆者定義了一個泛型類型為String的List集合。這樣一來List集合的泛型參數将會被定義為String類型。但是你有沒有想過,使用裡氏替換原則或者執行個體化泛型類型時,其實作可以簡化泛型類型聲明嗎?答案是肯定的,在Java7.x中,允許使用運算符“<>”來做類型推斷。也就是說你隻需要在聲明時标注泛型類型,實作時無需重複标注。

使用“<>”類型推斷運算符簡化泛型文法:

List<String> list = new ArrayList<>();    
           

三、變長參數方法的優化

Java7之前版本中的變長參數方法:

public int sum(int... args) {  
    int result = ;  
    for (int value : args) {  
        result += value;  
    }  
    return result;  
}  
           

當參數為不可具體化的類型時,如List,編譯器将産生警告,需要使用@SuppressWarnings(“unchecked”)注解聲明;Java7中使用@SafeVarargs注解抑制編譯器警告。

@SafeVarargs  
public static <T> T useVarargs(T... args) {  
    return args.length >  ? args[] : null;  
}  
           

三、字面值下劃線支援

不知道大家有沒有過同筆者一樣的煩惱,早在Java7.x版本之前,咱們在定義int或者long類型等變量的字面值時,往往會因為其定義的值過長,進而嚴重影響後續的可讀性。如果你也是這麼覺得,那麼你可以考慮使用Java7.x為字面值操作提供的可讀性優化。那便是允許你直接的字面值中使用符号“”進行切分,這樣一來不僅可以提升可讀性,還能夠清晰的分辨出字面值的長度。當然程式運作時自然會将“”符号進行提取再做運算。

int money = 100_000_000;

四、switch字面值支援

Java一共為開發人員提供了2種多路分支語句,一種是大家常用的if-else,另一種則是switch語句。早在Java7.x版本之前,switch語句表達式值隻能定義byte、short、int和char等4種類型,且該語句表達式值隻能比對一個,故不能重複。但是Java7.x的到來允許switch定義另一種全新的表達式值,那就是String類型。

public String generate(String name, String gender) {  
   String title = "";  
   switch (gender) {  
       case "男":  
           title = name + " 先生";  
           break;  
       case "女":  
           title = name + " 女士";  
           break;  
       default:  
           title = name;  
   }  
   return title;  
           

編譯器在編譯時先做處理: case隻有一種情況,直接轉成if; 如果隻有一個case和default,則直接轉換為if…else…; 有多個case,先将String轉換為hashCode,然後對應的進行處理,JavaCode在底層相容Java7以前版本。

五、聲明二進制字面值

Java與C語言、C++語言直接相關。Java語言繼承了C語言的文法結構,而OMT(Object Modeling Technique,對象模型)則是直接從C++語言改編而來的。是以早在Java7.x版本之前,開發人員隻能夠定義十進制、八進制、十六進制等字面值。但是現在你完全可以使用“0b”字元為字首定義二進制字面值。

Java7前支援十進制(123)、八進制(0123)、十六進制(0X12AB)

Java7增加二進制表示(0B11110001、0b11110001)

int test = 0b010101;

當然這裡筆者需要提示你的是,雖然咱們可以直接在程式中定義二進制字面值。但是在程式運算時,仍然會将其轉換成十進制展開運算和輸出。

六、catch表達式調整

談到catch語句的時候,不得不提到try語句,因為它們彼此之間存在互相依賴、互相關聯的關系。在Java程式中捕獲一個異常采用的是try和catch語句,try語句裡面所包含的代碼塊都是需要進行異常監測的,而catch語句裡面所包含的代碼塊,則是告訴程式當異常發生的時候所需要執行的異常處理。

談到捕獲異常,在Java7.x之前有2種方式。第一種是采用定義多個catch代碼塊,另外一種則是直接使用Exception(可恢複性異常超類)進行捕獲。但是現在,如果你覺得不想籠統的将所有異常定義為Exception進行捕獲,或者糾結于反複定義catch代碼塊,那麼你完全可以采用Java7.x的catch表達式調整。Java7.x允許你在catch表達式内部使用“|”運算符比對多個異常類型,當觸發異常時,異常類型将自動進行類型比對操作。

try { } catch (NumberFormatException | RuntimeException e) { }

①Throwable類增加addSuppressed方法和getSuppressed方法,支援原始異常中加入被抑制的異常。

異常抑制:在try和finally中同時抛出異常時,finally中抛出的異常會在異常棧中向上傳遞,而try中産生的原始異常會消失。

在Java7之前的版本,可以将原始異常儲存,在finally中産生異常時抛出原始異常:

public void read(String filename) throws BaseException {  
    FileInputStream input = null;  
    IOException readException = null;  
    try {  
        input = new FileInputStream(filename);  
    } catch (IOException ex) {  
        readException = ex;   //儲存原始異常  
    } finally {  
        if (input != null) {  
            try {  
                input.close();  
            } catch (IOException ex) {  
                if (readException == null) {  
                    readException = ex;  
                }  
            }  
        }  
        if (readException != null) {  
            throw new BaseException(readException);  
        }  
    }  
}  
           

在Java7中的版本,可以使用addSuppressed方法記錄被抑制的異常:

public void read(String filename) throws IOException {  
    FileInputStream input = null;  
    IOException readException = null;  
    try {  
        input = new FileInputStream(filename);  
    } catch (IOException ex) {  
        readException = ex;  
    } finally {  
        if (input != null) {  
            try {  
                input.close();  
            } catch (IOException ex) {  
                if (readException != null) {    //此處的差別  
                    readException.addSuppressed(ex);  
                }  
                else {  
                    readException = ex;  
                }  
            }  
        }  
        if (readException != null) {  
            throw readException;  
        }  
    }  
}  
           

七、檔案系統改變

既然本章節咱們已經談到了Java的檔案系統(FileSystem),那麼必然同樣也會關聯到I/O技術。其實所謂I/O(Input/Output)指的就是資料輸入/輸出的過程,我們稱之為流(資料通信通道)這個概念。比如當Java應用程式需要讀取目标資料源的資料時,則開啟輸入流。需要寫入時,則開啟輸出流。資料源允許是本地磁盤、記憶體或者是網絡中的資料。

向目标資料源讀取資料:

向目标資料源寫入資料:

Java的檔案系統主要由java.io及java.nio兩個包内的元件構成。早在Java7.x之前,檔案的操作一向都比較棘手。當然筆者這裡提出的棘手,更多的是指向Java API對檔案的管理的不便。比如咱們需要編寫一個程式,這個程式的功能僅僅隻是拷貝檔案後進行粘貼。但就是連這樣簡單的程式邏輯實作,開發人員動則都需要編寫幾十行有效代碼。

BufferedInputStream reader = new BufferedInputStream(   new FileInputStream(COPYFILEPATH));  
byte[] content = new byte[reader.available()];  
reader.read(content);  

BufferedOutputStream write = new BufferedOutputStream(  new FileOutputStream(PASTEFILEPATH));  
write.write(content);  
           

通過上述程式示例我們可以看出,僅僅隻是編寫一個簡單的檔案複制粘貼邏輯,我們的代碼量都大得驚人。如果你也認同上述程式的繁瑣,那麼你完全有必要體驗下Java7.x對檔案系統的一次全新改變。

Java7.x推出了全新的NIO.2 API以此改變針對檔案管理的不便,使得在java.nio.file包下使用Path、Paths、Files、WatchService、FileSystem等常用類型可以很好的簡化開發人員對檔案管理的編碼工作。

咱們就先從Path接口開始進行講解吧。Path接口的某些功能其實可以和java.io包下的File類型等價,當然這些功能僅限于隻讀操作。在實際開發過程中,開發人員可以聯用Path接口和Paths類型,進而擷取檔案的一系列上下文資訊。

Path接口常用方法如下:

方法名稱 方法傳回類型 方法描述

getNameCount()  int 擷取目前檔案節點數
getFileName()   java.nio.file.Path  擷取目前檔案名稱
getRoot()   java.nio.file.Path  擷取目前檔案根目錄
getParent() java.nio.file.Path  擷取目前檔案上級關聯目錄
聯用Path接口和Paths類型擷取檔案資訊:
@Test  
public void testFile() {  
    Path path = Paths.get("路徑:/檔案");  
    System.out.println("檔案節點數:" + path.getNameCount());     
    System.out.println("檔案名稱:" + path.getFileName());     
    System.out.println("檔案根目錄:" + path.getRoot());     
    System.out.println("檔案上級關聯目錄:" + path.getParent());     
}  
           

通過上述程式示例我們可以看出,聯用Path接口和Paths類型可以很友善的通路到目标檔案的上下文資訊。當然這些操作全都是隻讀的,如果開發人員想對檔案進行其它非隻讀操作,比如檔案的建立、修改、删除等操作,則可以使用Files類型進行操作。

Files類型常用方法如下:

方法名稱    方法傳回類型  方法描述
createFile()    java.nio.file.Path  在指定的目标目錄建立新檔案
delete()    void    删除指定目标路徑的檔案或檔案夾
copy()  java.nio.file.Path  将指定目标路徑的檔案拷貝到另一個檔案中
move()  java.nio.file.Path  将指定目标路徑的檔案轉移到其他路徑下,并删除源檔案
           

使用Files類型複制、粘貼檔案示例:

通過上述程式示例我們可以看出,使用Files類型來管理檔案,相對于傳統的I/O方式來說更加友善和簡單。因為具體的操作實作将全部移交給NIO.2 API,開發人員則無需關注。

Java7.x還為開發人員提供了一套全新的檔案系統功能,那就是檔案監測。在此或許有很多朋友并不知曉檔案監測有何意義及目,那麼請大家回想下調試成熱釋出功能後的Web容器。當項目疊代後并重新部署時,開發人員無需對其進行手動重新開機,因為Web容器一旦監測到檔案發生改變後,便會自動去适應這些“變化”并重新進行内部裝載。Web容器的熱釋出功能同樣也是基于檔案監測功能,是以不得不承認,檔案監測功能的出現對于Java檔案系統來說是具有重大意義的。

如果在程式中需要使用Java7.x的檔案監測功能,那麼我們務必需要了解java.nio.file包下的WatchService接口。WatchService接口不僅作為監測服務,還管理着具體的監控細節。

我們可以通過使用java.nio.file包下的FileSystems類型,并調用FileSystems類型的newWatchService()方法,進而擷取到WatchService接口的對象執行個體。

擷取WatchService接口執行個體:

WatchService watchService = FileSystems.getDefault() .newWatchService();

檔案監測是基于事件驅動的,事件觸發是作為監測的先決條件。開發人員可以使用java.nio.file包下的StandardWatchEventKinds類型提供的3種字面常量來定義監測事件類型,值得注意的是監測事件需要和WatchService執行個體一起進行注冊。

StandardWatchEventKinds類型提供的監測事件:

1、ENTRY_CREATE:檔案或檔案夾建立事件;

2、ENTRY_DELETE:檔案或檔案夾删除事件;

3、ENTRY_MODIFY:檔案或檔案夾粘貼事件;

使用WatchService類型實作檔案監控完整示例:

@Test  
public void testWatch() {  
    Path path = Paths.get("C:/");  
    try {  
        WatchService watchService = FileSystems.getDefault().newWatchService();  
        path.register(watchService, ENTRY_CREATE, ENTRY_DELETE,   ENTRY_MODIFY);  
        while (true) {  
            WatchKey watchKey = watchService.take();  
            for (WatchEvent<?> event : watchKey.pollEvents())  
                System.out.println(event.context().toString() + " 事件類型:"  + event.kind());  
            if (!watchKey.reset())  
                return;  
        }  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}  
           

通過上述程式示例我們可以看出,使用WatchService接口進行檔案監控非常簡單和友善。首先我們需要定義好目标監控路徑,然後調用FileSystems類型的newWatchService()方法建立WatchService對象。接下來我們還需使用Path接口的register()方法注冊WatchService執行個體及監控事件。當這些基礎作業層全部準備好後,我們再編寫外圍實時監測循環。最後疊代WatchKey來擷取所有觸發監控事件的檔案即可。

提示:

如果在項目中使用上述程式示例,筆者建議你最好将這段監控代碼進行異步化。因為死循環會占用主線程,後續代碼将永遠得不到執行機會。

八、探讨Java I/O模型

在基于分布式的程式設計環境中,系統性能的瓶頸往往由I/O讀寫性決定。這是不可否認的事實,也是衆多開發人員首要考慮的優化問題。如果在Windows環境下我們使用阻塞I/O模型來編寫分布式應用,其維護成本的往往超出你的想象。因為用戶端的連結數量直接決定了伺服器記憶體開辟的線程數量 * 2(包含:接受線程、輸出線程),并且這些線程是無法采取線程池優化的,因為線程的執行之間大于其建立和銷毀時間。長時間的大量并發線程挂起,不僅CPU要做實時任務切換,其整體實體資源都将一步步被蠶食,直至最後程式崩潰。在早期的網絡程式設計中,采取阻塞I/O模型來編寫分布式應用,唯一能做性能優化隻有采取傳統的硬體式堆機。在付出高昂的硬體成本開銷時,其項目的維護性也令開發人員頭痛。而且在實際的開發過程中,大部分開發人員會選擇将項目部署在Linux下運作。跟Windows核心結構不同的是,Linux環境下是沒有真正意義上的線程概念。其所謂的線程都是采用程序模拟的方式,也就是僞線程。筆者希望大家能夠明白,對于并發要求極高的分布式應用,一旦采用阻塞IO模型編寫就等于自尋死路。

Java的I/O模型由同步I/O和異步I/O構成。同步I/O模型包含:阻塞I/O和非阻塞I/O,而在Windows環境下隻要調用了IOCP的I/O模型,就是真正意義上的異步I/O。

IOCP(Input/Outut Completion Port,輸入/輸出完成端口)簡單來說是一種系統級的高性能異步I/O模型。應用程式中所有的I/O操作将全部委托給作業系統線程去執行,直至最後通知并傳回結果。Java7.x對IOCP進行了深度封裝,這使得開發人員可以使用IOCP API編寫高效的分布式應用。當然IOCP僅限于使用在Windows平台,因而無法在Linux平台上使用它(Linux平台上可以通過Epoll模拟IOCP實作)。

提示:

有過網絡程式設計經驗的開發人員都應該明白,在Windows平台下性能最好的I/O模型是IOCP,Linux平台下則是EPOLL。但是EPOLL并不算真正意義上的異步I/O,EPOLL隻是在盡可能的模拟IOCP而已。因為按照Unix網絡程式設計的劃分,多路複用I/O仍然屬于同步I/O模型,也就是說EPOLL其實是屬于多路複用I/O。

簡單來說異步I/O的特征必須滿足如下2點:

1、I/O請求與I/O操作不會阻塞;

2、并非程式自身完成I/O操作,由作業系統線程處理實際的I/O操作,直至最後通知并傳回結果;

早在Java4.x的時候,NIO(Java New Input/Output,Java新輸入/輸出)的出現,使得開發人員可以徹底從阻塞I/O的噩夢中掙脫出來。但編寫NIO的成本較大,學習難度也比較高,使得諸多開發人員望而卻步(目前比較成熟的NIO Frameworks有:Mina、Netty)。但了解非阻塞I/O的原理還是非常有必要,先來觀察下述采用阻塞I/O模式編寫的分布式應用示例:

@Test  
public void testServer() {  
    try {  
        ServerSocket server = new ServerSocket();  
        Socket clist = server.accept();  
        BufferedReader reader = new BufferedReader(new InputStreamReader(  clist.getInputStream()));  
        System.out.println(reader.readLine());  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}  
           

I/O的工作内容我們可以根據其性質劃分為2部分:I/O請求和I/O操作。上述程式示例我們采用的是阻塞I/O模型,可以很明确的發現當用戶端成功握手服務端後,如果服務端并沒有收到用戶端的I/O請求,服務端會在reader.readLine()方法處阻塞。直到成功接收到I/O請求後,服務端才會開始執行實際的I/O操作。運用阻塞I/O模式進行分布式程式設計,為了保證服務端與用戶端集合的成功會話,我們不得不為每一條用戶端連接配接都開辟獨立的線程執行I/O操作。當然在實際的開發過程中,或許已經沒有開發人員會做這麼荒唐的事情了。

非阻塞I/O和阻塞I/O最大的不同在于,非阻塞I/O并不會在I/O請求時産生阻塞。也就是說如果服務端沒有收到I/O請求時,非阻塞I/O會“持續輪循”I/O請求,當有請求進來後就開始執行I/O操作并阻塞請求程序。Java7.x允許開發人員使用IOCP API進行異步I/O程式設計,這使得開發人員不必再關心I/O是否阻塞。因為應用程式中所有的I/O操作将全部委托給作業系統線程去執行,直至最後通知并傳回結果。

九、Swing Framework(JSR 296規範)支援

筆者其實對Swing非常厭惡,如果可以的話筆者希望Oracle能夠廢除掉Swing這項技術。早在08年的時候筆者由于項目需要,曾飽受Swing的折磨。繁瑣的布局、元件加載優化、冗長代碼維護等這些令人痛苦和發指的問題,筆者相信使用過Swing的人開發人員都能發出相同的感歎。

早期的Java GUI(圖形使用者界面)主要由AWT技術主導,但随着使用者對胖用戶端技術的豐富度要求逐漸提高,AWT主鍵逐漸被Swing替代。Swing其實繼承于AWT,并提供有更加絢麗的視圖元件效果。何況相對于重量級的AWT元件來說,Swing顯得更加輕量。

筆者剛才說過,Swing雖然相對于AWT來說元件内容更加豐富,但仍然掩蓋不了其繁瑣的操作實作。如果對元件性能有過高要求,或者需要實作快速開發,筆者更建議你使用SWT或者JFace技術(無需指望使用IDE工具進行可視化程式設計,因為這純粹是吃力不讨好)。因為這2種技術可以看成是Swing的過渡,且相對Swing來說性能和豐富度更加優秀。

既然Java7.x對Swing仍不忘眷顧優化,那希望大家還是支援一下吧。從官方聲明可以看出,JSR 296規範的目标是簡化Swing的開發難度,且提供有更加豐富的元件資源。如果對于從未接觸過Swing程式設計的開發人員,筆者倒是建議你嘗試一下,或許你并不反感。

十、JVM核心更新之Class Loader架構調整

類裝載器(Class Loader)屬于JVM體系結構的重要組成部分,它是将Java類型裝載進JVM内部的關鍵一環。它使得Java類型可以動态的被裝載到JVM内部解釋并運作。

在JVM内部存在着2種類型的類裝載器:非自定義類裝載器和自定義類裝載器 。非自定義類裝載器負責裝載Java API中的類型及Java程式中的類型,而自定義類裝載器能夠使用自定義的方式來裝載其類型。不同類型的類裝載器所裝載的類型将被存放于JVM内部不同的命名空間中。

非自定義類裝載器由JVM内部3個核心類裝載器構成:

1、BootStrap ClassLoader;

2、ExtClassLoader;

3、AppClassLoader;

BootStrap ClassLoader也稱為啟動類裝載器,它是JVM内部最頂層的類裝載器。BootStrap ClassLoader主要負責裝載核心Java API中的類型。ExtClassLoader負責裝載擴充目錄下的類型。AppClassLoader則負責裝載ClassPath(Java應用類路徑)下指定的所有類型。其中ExtClassLoader和AppClassLoader都屬于BootStrap ClassLoader的派生類裝載器。

在Object内部封裝着一個通過JNI(Java Native Interface,Java本地接口)方式調用的getClass()本地方法,該方法傳回一個Class類型。對于開發人員而言,允許直接調用其getClassLoader()方法擷取類裝載器執行個體。

使用getClassLoader()方法擷取類裝載器:

@Test

public void testClassLoader() throws Exception {

ClassLoader loader = System.class.getClassLoader();

System.out.println(null != loader ? loader.getClass().getName() : loader);

System.out.println(CollationData_ar.class.getClassLoader().getClass().getName());

System.out.println(this.getClass().getClassLoader().getClass().getName());

}

通過上述程式示例我們可以看出,System類型是被BootStrap ClassLoader所裝載的。但程式的輸出結果卻是Null,當然這并不代表BootStrap ClassLoader不存在。因為BootStrap ClassLoader并不是采用Java語言編寫,而是由C++語言編寫并嵌入在JVM内部,是以開發人員無法擷取其執行個體。CollationData_ar類型屬于jre\lib\ext目錄下的派生類,由ExtClassLoader裝載。目前類則由AppClassLoader負責裝載。

在此筆者要提示大家,ExtClassLoader和AppClassLoader都是采用Java語言編寫。是以ExtClassLoader和AppClassLoader本身也都是Java類型,都會被最頂層的類裝載器BootStrap ClassLoader裝載,最後才會裝載各自管轄範圍内的類型。

談到ClassLoader的架構,我們不得不提及雙親委派模型。在JVM内部,類裝載器裝載類型所采用的便是雙親委派模型機制。比如AppClassLoader需要将一個類型裝載進JVM内部,首先其自身并不會立即裝載,而是将目标類型委派給上一級,也就是ExtClassLoader。ExtClassLoader接着再繼續委派給BootStrap ClassLoader。在JVM内部最頂層的類裝載器就是BootStrap ClassLoader,首先由它負責裝載目标類型及其關聯或依賴的所有類型。如果BootStrap ClassLoader裝載失敗,則退回給ExtClassLoader裝載。如果ExtClassLoader也無法裝載,最後隻能退回給AppClassLoader繼續裝載。最後當AppClassLoader都無法裝載的時候,便會抛出ClassNotFoundException異常(開發人員可以在捕獲ClassNotFoundException異常的時候重寫ClassLoder類型的findClass()方法實作自定義類型裝載)。

類裝載器架構示例:

類裝載器除了需要負責類型的裝載,還需要負責驗證目标類型的正确性、屬性記憶體配置設定、解析符号引用等操作。JVM通過裝載、連接配接和初始化一個Java類型,使其可以被運作時的Java應用程式所使用。其中裝載就是把二進制形式的Java類型寫入進JVM内部。連接配接則是把已經寫入進JVM中的二進制形式的類型合并到JVM的運作時狀态中去。然而連接配接階段又分成了3個步驟:驗證、準備和解析。“驗證”步驟確定了Java類型的資料格式,“準備”步驟則負責為目标類型配置設定所需的記憶體空間,“解析”步驟負責把常量池中的符号引用轉換為直接使用。“驗證”和“解析”這2個步驟都是為最後的初始化工作做準備。

類型生命周期示例:

Java7.x在上述ClassLoader架構的基礎之上,進行了一些細微調整。在早期開發人員如果想要實作自定義類裝載器,恐怕隻能實作ClassLoader類型并重寫其findClass()方法。但由于findClass()方法是按照串行結構的方式執行,或許是出于對性能和安全的考慮。Java7.x提供了一個擁有并行執行能力的增強實作,這樣一來自定義類裝載器便可以通過異步方式對類型進行裝載。

Java 8

概述

Java 8包含兩個主要項目:

1. Lambda

在Lambda項目中,多核處理器下的Java程式設計将更高效,Lambda表達式可以幫助開發人員提高效率,更好地利用多核處理器。Lambda項目還包括一個新的處理系統,該系統允許要求代碼模組化的程式設計模式作為資料。新功能一覽:

•Lambda表達式的運用

•擴充目标類型化

•方法和構造函數參考

•預設方法

2. Jigsaw

Jigsaw項目的目标是建立一個實用的方式來在JDK上設計和實施一個子產品系統,然後将該系統應用于JDK本身。其關鍵是令大塊的代碼更易于管理,并促進應用和大型運算的代碼重用。Jigsaw項目還帶來了許多新的表單功能,涉及封裝、重構、版本和子產品內建。

此外,除了這兩個項目,Java 8 還增加改進了一些其他語言功能,如更新核心Java庫使并行運算的表達更容易;虛拟擴充方法允許對接口增加方法,為預設實作指定參考;增加新的日期/時間API,同時支援傳感器,增加代碼的部署選項。

新特性

Lambdas

一個函數式接口非常有價值的屬性就是他們能夠用lambdas來執行個體化。這裡有一些lambdas的例子:
(int x, int y) -> { return x + y; }
    左邊是指定類型的逗号分割的輸入清單,右邊是帶有return的代碼塊
(x, y) -> x + y 左邊是推導類型的逗号分割的輸入清單,右邊是傳回值
x -> x * x
    左邊是推導類型的單一參數,右邊是一個傳回值
() -> x
    左邊沒有輸入 (官方名稱: "burger arrow"),在右邊傳回一個值
x -> { System.out.println(x); } 左邊是推導類型的單一參數,右邊是沒傳回值的代碼塊(傳回void)
String::valueOf
    靜态方法引用
Object::toString
    非靜态方法引用
x::toString
    繼承的函數引用
ArrayList::new
    構造函數引用

方法引用     等價的lambda表達式
String::valueOf x -> String.valueOf(x)
Object::toString    x -> x.toString()
x::toString () -> x.toString()
ArrayList::new  () -> new ArrayList<>()
當然,在Java裡方法能被重載。類可以有多個同名但不同參數的方法。這同樣對構造方法有效。ArrayList::new能夠指向它的個構造方法中任何一個。決定使用哪個方法是根據在使用的函數式接口。
一個lambda和給定的函數式接口在“外型”比對的時候相容。通過“外型”,我指向輸入、輸出的類型和聲明檢查異常。
給出兩個具體有效的例子:
Comparator<String> c = (a, b) -> Integer.compare(a.length(),b.length());
一個Comparator<String>的compare方法需要輸入兩個闡述,然後傳回一個int。這和lambda右側的一緻,是以這個任務是有效的。
Runnable r = () -> { System.out.println("Running!"); }
一個Runnable的run方法不需要參數也不會傳回值。這和lambda右側一緻,是以任務有效。
在抽象方法的簽名裡的受檢查異常(如果存在)也很重要。如果函數式接口在它的簽名裡聲明了異常,lambda隻能抛出受檢查異常。
捕獲和非捕獲的Lambda表達式
當Lambda表達式通路一個定義在Lambda表達式體外的非靜态變量或者對象時,這個Lambda表達式稱為“捕獲的”。比如,下面這個lambda表達式捕捉了變量x:
int x = ; return y -> x + y;
為了保證這個lambda表達式聲明是正确的,被它捕獲的變量必須是“有效final”的。是以要麼它們需要用final修飾符号标記,要麼保證它們在指派後不能被改變。
Lambda表達式是否是捕獲的和性能悄然相關。一個非不捕獲的lambda通常比捕獲的更高效,雖然這一點沒有書面的規範說明(據我所知),而且也不能為了程式的正确性指望它做什麼,非捕獲的lambda隻需要計算一次. 然後每次使用到它都會傳回一個唯一的執行個體。而捕獲的lambda表達式每次使用時都需要重新計算一次,而且從目前實作來看,它很像執行個體化一個匿名内部類的執行個體。
Non-final* 變量捕獲 - 如果一個變量被賦予新的數值,它将不能被用于lambda之中。"final"關鍵字不是必需的,但變量必須是“有效final”的(前面讨論過)。這個代碼不會被編譯:
int count = ;
List<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
    count++; // error: can't modify the value of count });
例外的透明度 - 如果一個已檢測的例外可能從lambda内部抛出,功能性的接口也必須聲明已檢測例外可以被抛出。這種例外不會散布到其包含的方法。這個代碼不會被編譯:
void appendAll(Iterable<String> values, Appendable out) throws IOException { 
// doesn't help with the error 
values.forEach(s -> {
out.append(s); 
// error: can't throw IOException here 
// Consumer.accept(T) doesn't allow it 
});
}
•   Function<T, R> -T作為輸入,傳回的R作為輸出
•   Predicate<T> -T作為輸入,傳回的boolean值作為輸出
•   Consumer<T> - T作為輸入,執行某種動作但沒有傳回值
•   Supplier<T> - 沒有任何輸入,傳回T
•   BinaryOperator<T> -兩個T作為輸入,傳回一個T作為輸出,對于“reduce”操作很有用
這些最原始的特征同樣存在。他們以int,long和double的方式提供。例如:
•   IntConsumer -以int作為輸入,執行某種動作,沒有傳回值
這裡存在性能上的一些原因,主要釋在輸入或輸出的時候避免裝箱和拆箱操作。
           

接口改善

現在接口裡已經完全可以定義靜态方法了.

interface Formula {

double calculate(int a);

default double sqrt(int a) {

return Math.sqrt(a);

}

}

Formula接口在擁有calculate方法之外同時還定義了sqrt方法,實作了Formula接口的子類隻需要實作一個calculate方法,預設方法sqrt将在子類上可以直接使用。

Formula formula = new Formula() {

@Override

public double calculate(int a) {

return sqrt(a * 100);

}

};

formula.calculate(100); // 100.0

formula.sqrt(16); // 4.0

文中的formula被實作為一個匿名類的執行個體,該代碼非常容易了解,6行代碼實作了計算 sqrt(a * 100)。

Java 8中接口可以定義預設的方法了.舉個例子,一個for-each循環的方法就可以加入到java.lang.Iterable中:

public default void forEach(Consumer

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});
           

隻需要給靜态方法 Collections.sort 傳入一個List對象以及一個比較器來按指定順序排列。通常做法都是建立一個匿名的比較器對象然後将其傳遞給sort方法。

在Java 8 中你就沒必要使用這種傳統的匿名對象的方式了,Java 8提供了更簡潔的文法,lambda表達式:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});
           

看到了吧,代碼變得更段且更具有可讀性,但是實際上還可以寫得更短:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

對于函數體隻有一行代碼的,你可以去掉大括号{}以及return關鍵字,但是你還可以寫得更短點:

Collections.sort(names, (a, b) -> b.compareTo(a));

Java編譯器可以自動推導出參數類型,是以你可以不用再寫一次類型。接下來我們看看lambda表達式還能作出什麼更友善的東西來:

方法與構造函數引用

前一節中的代碼還可以通過靜态方法引用來表示:

Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123
           

Java 8 允許你使用 :: 關鍵字來傳遞方法或者構造函數引用,上面的代碼展示了如何引用一個靜态方法,我們也可以引用一個對象的方法:

converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"
           

接下來看看構造函數是如何使用::關鍵字來引用的,首先我們定義一個包含多個構造函數的簡單類:

class Person {
    String firstName;
    String lastName;
    Person() {}
    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
           

接下來我們指定一個用來建立Person對象的對象工廠接口:

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}
           

這裡我們使用構造函數引用來将他們關聯起來,而不是實作一個完整的工廠:

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
           

我們隻需要使用 Person::new 來擷取Person類構造函數的引用,Java編譯器會自動根據PersonFactory.create方法的簽名來選擇合适的構造函數。

函數式接口

Java 8 引入的一個核心概念是函數式接口。如果一個接口定義個唯一一個抽象方法,那麼這個接口就成為函數式接口。比如,java.lang.Runnable就是一個函數式接口,因為它隻定義一個抽象方法:

public abstract void run();

留意到“abstract”修飾詞在這裡是隐含的,因為這個方法缺少方法體。為了表示一個函數式接口,并非像這段代碼一樣一定需要“abstract”關鍵字。

預設方法不是abstract的,是以一個函數式接口裡可以定義任意多的預設方法,這取決于你。

同時,引入了一個新的Annotation:@FunctionalInterface。可以把他它放在一個接口前,表示這個接口是一個函數式接口。加上它的接口不會被編譯,除非你設法把它變成一個函數式接口。它有點像@Override,都是聲明了一種使用意圖,避免你把它用錯。

Lambda表達式是如何在java的類型系統中表示的呢?每一個lambda表達式都對應一個類型,通常是接口類型。而“函數式接口”是指僅僅隻包含一個抽象方法的接口,每一個該類型的lambda表達式都會被比對到這個抽象方法。因為 預設方法 不算抽象方法,是以你也可以給你的函數式接口添加預設方法。

我們可以将lambda表達式當作任意隻包含一個抽象方法的接口類型,確定你的接口一定達到這個要求,你隻需要給你的接口添加 @FunctionalInterface 注解,編譯器如果發現你标注了這個注解的接口有多于一個抽象方法的時候會報錯的。

示例如下:

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123
           

需要注意如果@FunctionalInterface如果沒有指定,上面的代碼也是對的。

五、Lambda 作用域

在lambda表達式中通路外層作用域和老版本的匿名對象中的方式很相似。你可以直接通路标記了final的外層局部變量,或者執行個體的字段以及靜态變量。

final int num = ;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert();     // 3
           

但是和匿名對象不同的是,這裡的變量num可以不用聲明為final,該代碼同樣正确:

int num = ;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert();     // 3
           

不過這裡的num必須不可被後面的代碼修改(即隐性的具有final的語義),例如下面的就無法編譯:

int num = ;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
num = ;
           

在lambda表達式中試圖修改num同樣是不允許的。

和本地變量不同的是,lambda内部對于執行個體的字段以及靜态變量是即可讀又可寫。該行為和匿名對象是一緻的:

class Lambda4 {
    static int outerStaticNum;
    int outerNum;
    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = ;
            return String.valueOf(from);
        };
        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = ;
            return String.valueOf(from);
        };
    }
}
           

八、通路接口的預設方法

還記得formula例子麼,接口Formula定義了一個預設方法sqrt可以直接被formula的執行個體包括匿名對象通路到,但是在lambda表達式中這個是不行的。

Lambda表達式中是無法通路到預設方法的,以下代碼将無法編譯:

Formula formula = (a) -> sqrt( a * );
Built-in Functional Interfaces
           

JDK 1.8 API包含了很多内建的函數式接口,在老Java中常用到的比如Comparator或者Runnable接口,這些接口都增加了@FunctionalInterface注解以便能用在lambda上。

Java 8 API函數式接口

Java 8 API同樣還提供了很多全新的函數式接口來讓工作更加友善,有一些接口是來自Google Guava庫裡的,即便你對這些很熟悉了,還是有必要看看這些是如何擴充到lambda上使用的。

Predicate接口

Predicate 接口隻有一個參數,傳回boolean類型。該接口包含多種預設方法來将Predicate組合成其他複雜的邏輯(比如:與,或,非):

Predicate<String> predicate = (s) -> s.length() > ;
predicate.test("foo");              // true
predicate.negate().test("foo");     // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
           

Function 接口

Function 接口有一個參數并且傳回一個結果,并附帶了一些可以和其他函數組合的預設方法(compose, andThen):
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"
           

Supplier 接口

Supplier 接口傳回一個任意範型的值,和Function接口不同的是該接口沒有任何參數
Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person
           

Consumer 接口

Consumer 接口表示執行在單個參數上的操作。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
           

Comparator 接口

Comparator 是老Java中的經典接口, Java 在此之上添加了多種預設方法:
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0
           

Optional 接口

Optional 不是函數是接口,這是個用來防止NullPointerException異常的輔助類型, Optional 被定義為一個簡單的容器,其值可能是null或者不是null。在Java 之前一般某個函數應該傳回非空對象但是偶爾卻可能傳回了null,而在Java 中,不推薦你傳回null而是傳回Optional。
Optional<String> optional = Optional.of("bam");
optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt()));     // "b"
           

Stream 接口

java.util.Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分為中間操作或者最終操作兩種,最終操作傳回一特定類型的計算結果,而中間操作傳回Stream本身,這樣你就可以将多個操作依次串起來。Stream 的建立需要指定一個資料源,比如 java.util.Collection的子類,List或者Set, Map不支援。Stream的操作可以串行執行或者并行執行。
首先看看Stream是怎麼用,首先建立執行個體代碼的用到的資料List:
List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");
           

Java 8擴充了集合類,可以通過 Collection.stream() 或者 Collection.parallelStream() 來建立一個Stream。下面幾節将詳細解釋常用的Stream操作:

Filter 過濾

過濾通過一個predicate接口來過濾并隻保留符合條件的元素,該操作屬于中間操作,是以我們可以在過濾後的結果來應用其他Stream操作(比如forEach)。forEach需要一個函數來對過濾後的元素依次執行。forEach是一個最終操作,是以我們不能在forEach之後來執行其他Stream操作。

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa2", "aaa1"
           

Sort 排序

排序是一個中間操作,傳回的是排序好後的Stream。如果你不指定一個自定義的Comparator則會使用預設排序。

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa1", "aaa2"
           

需要注意的是,排序隻建立了一個排列好後的Stream,而不會影響原有的資料源,排序之後原資料stringCollection是不會被修改的:

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
           

Map 映射

中間操作map會将元素根據指定的Function接口來依次将元素轉成另外的對象,下面的示例展示了将字元串轉換為大寫字元串。你也可以通過map來講對象轉換成其他類型,map傳回的Stream類型是根據你map傳遞進去的函數的傳回值決定的。

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
           

Match 比對

Stream提供了多種比對操作,允許檢測指定的Predicate是否比對整個Stream。所有的比對操作都是最終操作,并傳回一個boolean類型的值。

boolean anyStartsWithA = 
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA);      // true
boolean allStartsWithA = 
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA);      // false
boolean noneStartsWithZ = 
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ);      // true
           

Count 計數

計數是一個最終操作,傳回Stream中元素的個數,傳回值類型是long。

long startsWithB = 
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();
System.out.println(startsWithB);    // 3

           

Reduce 規約

這是一個最終操作,允許通過指定的函數來講stream中的多個元素規約為一個元素,規越後的結果是通過Optional接口表示的:

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
           

并行Streams

前面提到過Stream有串行和并行兩種,串行Stream上的操作是在一個線程中依次完成,而并行Stream則是在多個線程上同時執行。

下面的例子展示了是如何通過并行Stream來提升性能:

//首先我們建立一個沒有重複元素的大表:
int max = ;
List<String> values = new ArrayList<>(max);
for (int i = ; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}
//然後我們計算一下排序這個Stream要耗時多久,串行排序:
long t0 = System.nanoTime();
long count = values.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
// 串行耗時:  ms
//并行排序:
long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
// 并行排序耗時:  ms
           

上面兩個代碼幾乎是一樣的,但是并行版的快了50%之多,唯一需要做的改動就是将stream()改為parallelStream()。

Map

前面提到過,Map類型不支援stream,不過Map提供了一些新的有用的方法來處理一些日常任務。

Map<Integer, String> map = new HashMap<>();
for (int i = ; i < ; i++) {
    map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));
           

以上代碼很容易了解, putIfAbsent 不需要我們做額外的存在性檢查,而forEach則接收一個Consumer接口來對map裡的每一個鍵值對進行操作。

下面的例子展示了map上的其他有用的函數:

map.computeIfPresent(, (num, val) -> val + num);
map.get();             // val33
map.computeIfPresent(, (num, val) -> null);
map.containsKey();     // false
map.computeIfAbsent(, num -> "val" + num);
map.containsKey();    // true
map.computeIfAbsent(, num -> "bam");
map.get();             // val33
           

接下來展示如何在Map裡删除一個鍵值全都比對的項:

map.remove(, "val3");
map.get();             // val33
map.remove(, "val33");
map.get();             // null
           

另外一個有用的方法:

map.getOrDefault(42, “not found”); // not found

對Map的元素做合并也變得很容易了:

map.merge(, "val9", (value, newValue) -> value.concat(newValue));
map.get();             // val9
map.merge(, "concat", (value, newValue) -> value.concat(newValue));
map.get();             // val9concat
           

Merge做的事情是如果鍵名不存在則插入,否則則對原鍵對應的值做合并操作并重新插入到map中。

Date API

Java 8 在包java.time下包含了一組全新的時間日期API。新的日期API和開源的Joda-Time庫差不多,但又不完全一樣,下面的例子展示了這組新API裡最重要的一些部分:

Clock 時鐘

Clock類提供了通路目前日期和時間的方法,Clock是時區敏感的,可以用來取代 System.currentTimeMillis() 來擷取目前的微秒數。某一個特定的時間點也可以使用Instant類來表示,Instant類也可以用來建立老的java.util.Date對象。

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date
           

Timezones 時區

在新API中時區使用ZoneId來表示。時區可以很友善的使用靜态方法of來擷取到。 時區定義了到UTS時間的時間差,在Instant時間點對象到本地日期對象之間轉換的時候是極其重要的。

System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+:]
// ZoneRules[currentStandardOffset=-:]
           

LocalTime 本地時間

LocalTime 定義了一個沒有時區資訊的時間,例如 晚上10點,或者 17:30:15。下面的例子使用前面代碼建立的時區建立了兩個本地時間。之後比較時間并以小時和分鐘為機關計算兩個時間的時間差:

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2));  // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

LocalTime 提供了多種工廠方法來簡化對象的建立,包括解析時間字元串。
LocalTime late = LocalTime.of(, , );
System.out.println(late);       // 23:59:59
DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37
           

LocalDate 本地日期

LocalDate 表示了一個确切的日期,比如 2014-03-11。該對象值是不可變的,用起來和LocalTime基本一緻。下面的例子展示了如何給Date對象加減天/月/年。另外要注意的是這些對象是不可變的,操作傳回的總是一個新執行個體。

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays();
LocalDate independenceDay = LocalDate.of(, Month.JULY, );
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek);    // FRIDAY
從字元串解析一個LocalDate類型和解析LocalTime一樣簡單:
DateTimeFormatter germanFormatter = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

LocalDateTime 本地日期時間
LocalDateTime 同時表示了時間和日期,相當于前兩節内容合并到一個對象上了。LocalDateTime和LocalTime還有LocalDate一樣,都是不可變的。LocalDateTime提供了一些能通路具體字段的方法。
LocalDateTime sylvester = LocalDateTime.of(, Month.DECEMBER, , , , );
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439
           

隻要附加上時區資訊,就可以将其轉換為一個時間點Instant對象,Instant時間點對象可以很容易的轉換為老式的java.util.Date。

Instant instant = sylvester

.atZone(ZoneId.systemDefault())

.toInstant();

Date legacyDate = Date.from(instant);

System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

格式化LocalDateTime和格式化時間和日期一樣的,除了使用預定義好的格式外,我們也可以自己定義格式:

DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13
           

和java.text.NumberFormat不一樣的是新版的DateTimeFormatter是不可變的,是以它是線程安全的。

關于時間日期格式的詳細資訊:http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html

十、Annotation 注解

在Java 8中支援多重注解了,先看個例子來了解一下是什麼意思。

首先定義一個包裝類Hints注解用來放置一組具體的Hint注解:

@interface Hints {
    Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
    String value();
}
           

Java 8允許我們把同一個類型的注解使用多次,隻需要給該注解标注一下@Repeatable即可。

例 1: 使用包裝類當容器來存多個注解(老方法)

@Hints({@Hint(“hint1”), @Hint(“hint2”)})

class Person {}

例 2:使用多重注解(新方法)

@Hint(“hint1”)

@Hint(“hint2”)

class Person {}

第二個例子裡java編譯器會隐性的幫你定義好@Hints注解,了解這一點有助于你用反射來擷取這些資訊:

Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint);                               // null
Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length);          // 2
Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length);                  // 2
           

即便我們沒有在Person類上定義@Hints注解,我們還是可以通過 getAnnotation(Hints.class) 來擷取 @Hints注解,更加友善的方法是使用 getAnnotationsByType 可以直接擷取到所有的@Hint注解。

另外Java 8的注解還增加到兩種新的target上了:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})

@interface MyAnnotation {}

關于Java 8的新特性就寫到這了,肯定還有更多的特性等待發掘。JDK 1.8裡還有很多很有用的東西,比如Arrays.parallelSort, StampedLock和CompletableFuture等等。

Java 9、10的發展規劃

甲骨文對Java 8 的前景很是看好,并已經開始讨論Java 9發展的關鍵領域。比如加入一個self-tuning JVM,提高本地內建和大規模多核的可擴充性;通過新的元對象協定和資料總管為雲應用添加跨語言支援。

甲骨文也表示,Java9和10将加入大資料、多語言的互操作性、雲計算和移動,預期分别于2015年和2017年釋出。而關于Java開發工具包(JDK)10以及之後的版本也正在讨論中,比如使Java語言面向對象,形成一個統一的類型系統,所有原語都将轉換為對象和方法。

随着使用人數的增加,Java正逐漸成為最常用的程式設計語言,令每個使用者都滿意成了它的目标。甲骨文認為Java在将來會成為開發者們首選的程式設計語言,因為它可以實作的東西正好符合了開發者們的期望。