《OnJava8》精讀(七)文件、字符串及泛型

在這裏插入圖片描述

介紹


《On Java 8》是什麼?

它是《Thinking In Java》的作者Bruce Eckel基於Java8寫的新書。裏面包含了對Java深入的理解及思想維度的理念。可以比作Java界的“武學祕籍”。任何Java語言的使用者,甚至是非Java使用者但是對面向對象思想有興趣的程序員都該一讀的經典書籍。目前豆瓣評分9.5,是公認的編程經典。

爲什麼要寫這個系列的精讀博文?

由於書籍讀起來時間久,過程漫長,因此產生了寫本精讀系列的最初想法。除此之外,由於中文版是譯版,讀起來還是有較大的生硬感(這種差異並非譯者的翻譯問題,類似英文無法譯出唐詩的原因),這導致我們理解作者意圖需要一點推敲。再加上原書的內容很長,只第一章就多達一萬多字(不含代碼),讀起來就需要大量時間。

所以,如果現在有一個人能替我們先仔細讀一遍,篩選出其中的精華,讓我們可以在地鐵上或者路上不用花太多時間就可以瞭解這邊經典書籍的思想那就最好不過了。於是這個系列誕生了。

一些建議

推薦讀本書的英文版原著。此外,也可以參考本書的中文譯版。我在寫這個系列的時候,會盡量的保證以“陳述”的方式表達原著的內容,也會寫出自己的部分觀點,但是這種觀點會保持理性並儘量少而精。本系列中對於原著的內容會以引用的方式體現。
最重要的一點,大家可以通過博客平臺的評論功能多加交流,這也是學習的一個重要環節。

第十七章 文件


本章總字數:6000
關鍵詞:

  • 文件和目錄
  • 文件查找
  • 文件讀寫

本章的內容不多,作者主要提到了文件的部分操作。一開始作者瘋狂吐槽了在Java7之前的IO模塊,認爲其“設計者毫不在意他們的使用者的體驗這一觀念”。

好在Java7之後,新的IO設計已經相當強大,特別是結合了Java8中的streams 使得文件操作更加尤雅。

文件和目錄

Path用來操作路徑相關的所有操作。

// files/PartsOfPaths.java
import java.nio.file.*;

public class PartsOfPaths {
    public static void main(String[] args) {
        System.out.println(System.getProperty("os.name"));
        Path p = Paths.get("PartsOfPaths.java").toAbsolutePath();
        for(int i = 0; i < p.getNameCount(); i++)
            System.out.println(p.getName(i));
        System.out.println("ends with '.java': " +
        p.endsWith(".java"));
        for(Path pp : p) {
            System.out.print(pp + ": ");
            System.out.print(p.startsWith(pp) + " : ");
            System.out.println(p.endsWith(pp));
        }
        System.out.println("Starts with " + p.getRoot() + " " + p.startsWith(p.getRoot()));
    }
}

結果:

Windows 10
Users
Bruce
Documents
GitHub
on-java
ExtractedExamples
files
PartsOfPaths.java
ends with '.java': false
Users: false : false
Bruce: false : false
Documents: false : false
GitHub: false : false
on-java: false : false
ExtractedExamples: false : false
files: false : false
PartsOfPaths.java: false : true
Starts with C:\ true

可以通過 getName() 來索引 Path 的各個部分,直到達到上限 getNameCount()。Path 也實現了 Iterable 接口,因此我們也可以通過增強的 for-each 進行遍歷。請注意,即使路徑以 .java 結尾,使用 endsWith() 方法也會返回 false。這是因爲使用 endsWith() 比較的是整個路徑部分,而不會包含文件路徑的後綴。通過使用 startsWith() 和 endsWith() 也可以完成路徑的遍歷。但是我們可以看到,遍歷 Path 對象並不包含根路徑,只有使用 startsWith() 檢測根路徑時纔會返回 true。

此外,Files 工具類有一系列獲取Path信息的方法。

    static void say(String id, Object result) {
        System.out.print(id + ": ");
        System.out.println(result);
    }
    ...
        Path p = Paths.get("PathAnalysis.java").toAbsolutePath();
        say("Exists", Files.exists(p));
        say("Directory", Files.isDirectory(p));
        say("Executable", Files.isExecutable(p));
        say("Readable", Files.isReadable(p));
        say("RegularFile", Files.isRegularFile(p));
        say("Writable", Files.isWritable(p));
        say("notExists", Files.notExists(p));
        say("Hidden", Files.isHidden(p));
        say("size", Files.size(p));
        say("FileStore", Files.getFileStore(p));
        say("LastModified: ", Files.getLastModifiedTime(p));
        say("Owner", Files.getOwner(p));
        say("ContentType", Files.probeContentType(p));
        say("SymbolicLink", Files.isSymbolicLink(p));

結果:

Exists: true
Directory: false
Executable: true
Readable: true
RegularFile: true
Writable: true
notExists: false
Hidden: false
size: 1631
FileStore: SSD (C:)
LastModified: : 2017-05-09T12:07:00.428366Z
Owner: MINDVIEWTOSHIBA\Bruce (User)
ContentType: null
SymbolicLink: false

WatchService 通過進程監視目錄。比如下一個示例中監視刪除目錄下.txt結尾的文件:

// files/PathWatcher.java
// {ExcludeFromGradle}
import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.util.concurrent.*;

public class PathWatcher {
    static Path test = Paths.get("test");

    static void delTxtFiles() {
        try {
            Files.walk(test)
            .filter(f ->
                f.toString()
                .endsWith(".txt"))
                .forEach(f -> {
                try {
                    System.out.println("deleting " + f);
                    Files.delete(f);
                } catch(IOException e) {
                    throw new RuntimeException(e);
                }
            });
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        Directories.refreshTestDir();
        Directories.populateTestDir();
        Files.createFile(test.resolve("Hello.txt"));
        WatchService watcher = FileSystems.getDefault().newWatchService();
        test.register(watcher, ENTRY_DELETE);
        Executors.newSingleThreadScheduledExecutor()
        .schedule(PathWatcher::delTxtFiles,
        250, TimeUnit.MILLISECONDS);
        WatchKey key = watcher.take();
        for(WatchEvent evt : key.pollEvents()) {
            System.out.println("evt.context(): " + evt.context() +
            "\nevt.count(): " + evt.count() +
            "\nevt.kind(): " + evt.kind());
            System.exit(0);
        }
    }
}

結果:

deleting test\bag\foo\bar\baz\File.txt
deleting test\bar\baz\bag\foo\File.txt
deleting test\baz\bag\foo\bar\File.txt
deleting test\foo\bar\baz\bag\File.txt
deleting test\Hello.txt
evt.context(): Hello.txt
evt.count(): 1
evt.kind(): ENTRY_DELETE

查看輸出的具體內容。即使我們正在刪除以 .txt 結尾的文件,在 Hello.txt 被刪除之前,WatchService 也不會被觸發。你可能認爲,如果說"監視這個目錄",自然會包含整個目錄和下面子目錄,但實際上:只會監視給定的目錄,而不是下面的所有內容。如果需要監視整個樹目錄,必須在整個樹的每個子目錄上放置一個 Watchservice。

文件查找

上文中一直通過目錄查找文件,實際上在java.nio.file中有更好的解決方案。

PathMatcher matcher = FileSystems.getDefault()
          .getPathMatcher("glob:**/*.{tmp,txt}");
PathMatcher matcher2 = FileSystems.getDefault()
          .getPathMatcher("glob:*.tmp");
Files.walk(test)
          .filter(matcher::matches)
          .forEach(System.out::println);
Files.walk(test)
          .filter(matcher2::matches)
          .forEach(System.out::println);          

通過在 FileSystem 對象上調用 getPathMatcher() 獲得一個 PathMatcher,然後傳入您感興趣的模式。模式有兩個選項:glob 和 regex。

在 matcher 中,glob 表達式開頭的 **/ 表示“當前目錄及所有子目錄”,這在當你不僅僅要匹配當前目錄下特定結尾的 Path 時非常有用。單 * 表示“任何東西”,然後是一個點,然後大括號表示一系列的可能性——我們正在尋找以 .tmp 或 .txt 結尾的東西。您可以在 getPathMatcher() 文檔中找到更多詳細信息。matcher2 只使用 *.tmp ,通常不匹配任何內容,但是添加 map() 操作會將完整路徑減少到末尾的名稱。
注意,在這兩種情況下,輸出中都會出現 dir.tmp,即使它是一個目錄而不是一個文件。要只查找文件,必須像在最後 files.walk() 中那樣對其進行篩選。

讀寫文件

java.nio.file.Files 對於較小的文件可以輕鬆讀寫。Files.readAllLines() 一次讀取整個文件。

// files/ListOfLines.java
import java.util.*;
import java.nio.file.*;

public class ListOfLines {
    public static void main(String[] args) throws Exception {
        Files.readAllLines(
        Paths.get("../streams/Cheese.dat"))
        .stream()
        .filter(line -> !line.startsWith("//"))
        .map(line ->
            line.substring(0, line.length()/2))
        .forEach(System.out::println);
    }
}

結果:

Not much of a cheese
Finest in the
And what leads you
Well, it's
It's certainly uncon

Files.write()用來寫入:

// files/Writing.java
import java.util.*;
import java.nio.file.*;

public class Writing {
    static Random rand = new Random(47);
    static final int SIZE = 1000;

    public static void main(String[] args) throws Exception {
        // Write bytes to a file:
        byte[] bytes = new byte[SIZE];
        rand.nextBytes(bytes);
        Files.write(Paths.get("bytes.dat"), bytes);
        System.out.println("bytes.dat: " + Files.size(Paths.get("bytes.dat")));

        // Write an iterable to a file:
        List<String> lines = Files.readAllLines(
          Paths.get("../streams/Cheese.dat"));
        Files.write(Paths.get("Cheese.txt"), lines);
        System.out.println("Cheese.txt: " + Files.size(Paths.get("Cheese.txt")));
    }
}

結果:

bytes.dat: 1000
Cheese.txt: 199

現在,我們也可以使用流(stream)來讀寫:

//讀取文件
FileInputStream Fis = new FileInputStream("C:\\jimmy.txt");
int i = Fis.read();
while(i!=-1) {
	i = Fis.read();
}
Ins.close();
//寫入文件
FileOutputStream fos = new FileOutputStream("C:\\jimmy.txt");
String str = "hi~Jimmy";
byte[] bt = str.getBytes();
fos.write(bt);	
fos.close(); 

第十八章 字符串

本章總字數:16000

關鍵詞:

  • 字符串是不可變的
  • +號的重載以及StringBuilder
  • 字符串操作
  • 格式化輸出

本章又是一個基礎但又重要的章節。原本我很疑惑爲什麼作者要把字符串這麼基礎的內容放到第十八章纔講(本書共二十五章節)。在後來讀完了內容慢慢有一點體會:字符串雖然是一個基礎類型,但同時又很特殊,它跟經常使用的int、Boolean、char等等都有很大區別。作爲非數值類型,String又可以使用+號,這裏又牽扯到符號重載的知識,以及String與數組之間的各種關係。更不用說常見的數值類型與字符串直接的相互轉化。這些都需要有一定基礎知識才能講明白。

字符串是不可變的

字符串的值一旦創建就不會改變。或許會覺不可思議,若對Java(或者C#等面嚮對象語言)有底層瞭解的人會更容易理解。

String 對象是不可變的,你可以給一個 String 對象添加任意多的別名。因爲 String 是隻讀的,所以指向它的任何引用都不可能修改它的值,因此,也就不會影響到其他引用。

初學者這時候可能會有點疑惑,比如這個例子中有幾個字符串:

String a1="a";
String b1="b";
String a=a1+b1;
String b="ab";
String c=new String("ab");

System.out.println(a==b);
System.out.println(a.equals(b));
System.out.println(c==b);
System.out.println(c.equals(b));

答案是4個:“a”、“b”、“ab”,“ab”

結果:

true
true
false
true

這個示例中的==用來比較對象的引用是否是同一個內存地址,而equals() 用來比較值是否相同。很顯然,對象c 由於使用了new 關鍵詞而重新創建了一個“ab”的內容引用。

爲了語言性能也爲了方便使用,Java使用了字符串永不改變的概念(C#也一樣)。字符串“a”和“b”在創建之後已經存在於堆中,在爲對象a 賦值時程序將“a”、“b”拼接成新的字符串“ab”放在堆中,原本的“a”和“b”字符串則保留下來還在原來的內存位置。一旦後面有新的字符串對象賦值爲“ab”,Java會將堆的地址指針直接賦值給該對象,而不需要重新創建。除非我們顯式的使用new關鍵詞創建字符串對象。

再來看看原著中的示例:

String q = "howdy";
System.out.println(q); // howdy 
String qq = upcase(q); 
System.out.println(qq); // HOWDY 
System.out.println(q); // howdy 

當把 q 傳遞給 upcase() 方法時,實際傳遞的是引用的一個拷貝。其實,每當把 String 對象作爲方法的參數時,都會複製一份引用,而該引用所指向的對象其實一直待在單一的物理位置上,從未動過。
回到 upcase() 的定義,傳入其中的引用有了名字 s,只有 upcase() 運行的時候,局部引用 s 才存在。一旦 upcase() 運行結束,s 就消失了。當然了,upcase() 的返回值,其實是最終結果的引用。這足以說明,upcase() 返回的引用已經指向了一個新的對象,而 q 仍然在原來的位置。

所以,現在你應該能夠理解“字符串是不可變的”含義了——不可變的是字符串對象指向的內存中的值。這些值一旦創建就會一直存在,並不意味字符串對象不可以改變。字符串 a也可以賦上新值,只不過此時賦值會創建一個新內存地址並賦予該對象引用。

字符串是非數值類型,但是可以使用+ 號,使用 +號表示將前後字符串的內容拼接。

如果你使用反編譯工具(javap )來看拼接字符串的代碼,你就會發現,Java在編譯過程中自動使用了 StringBuilder 。使用它可以更加優化字符串的操作效率。

看原著的一個例子:

// strings/UsingStringBuilder.java 

import java.util.*; 
import java.util.stream.*; 
public class UsingStringBuilder { 
    public static String string1() { 
        Random rand = new Random(47);
        StringBuilder result = new StringBuilder("["); 
        for(int i = 0; i < 25; i++) { 
            result.append(rand.nextInt(100)); 
            result.append(", "); 
        } 
        result.delete(result.length()-2, result.length()); 
        result.append("]");
        return result.toString(); 
    } 
    public static String string2() { 
        String result = new Random(47)
            .ints(25, 0, 100)
            .mapToObj(Integer::toString)
            .collect(Collectors.joining(", "));
        return "[" + result + "]"; 
    } 
    public static void main(String[] args) { 
        System.out.println(string1()); 
        System.out.println(string2()); 
    }
} 

結果:

[58, 55, 93, 61, 61, 29, 68, 0, 22, 7, 88, 28, 51, 89, 
9, 78, 98, 61, 20, 58, 16, 40, 11, 22, 4] 
[58, 55, 93, 61, 61, 29, 68, 0, 22, 7, 88, 28, 51, 89,
9, 78, 98, 61, 20, 58, 16, 40, 11, 22, 4] 

StringBuilder 提供了豐富而全面的方法,包括 insert()、replace()、substring(),甚至還有reverse(),但是最常用的還是 append() 和 toString()。還有 delete(),上面的例子中我們用它刪除最後一個逗號和空格,以便添加右括號。
string2() 使用了 Stream,這樣代碼更加簡潔美觀。可以證明,Collectors.joining() 內部也是使用的 > StringBuilder,這種寫法不會影響性能!

字符串操作

原著介紹的過多,此處只篩選出常用的。

方法 作用 參數,重載版本
構造方法 創建String對象 默認版本,String,StringBuilder,StringBuffer,char數組,byte數組
length() String中字符的個數
toCharArray() 生成一個char[],包含String中的所有字符
equals(),equalsIgnoreCase() 比較兩個String的內容是否相同。如果相同,結果爲true 與之進行比較的String
compareTo(),compareToIgnoreCase() 按詞典順序比較String的內容,比較結果爲負數、零或正數。注意,大小寫不等價 與之進行比較的String
isEmpty() 返回boolean結果,以表明String對象的長度是否爲0
indexOf(),lastIndexOf() 如果該String並不包含此參數,就返回-1;否則返回此參數在String中的起始索引。lastIndexOf()是從後往前搜索 重載版本包括:char,char與起始索引,String,String與起始索引
matches() 返回boolean結果,以表明該String和給出的正則表達式是否匹配 一個正則表達式
split() 按照正則表達式拆分String,返回一個結果數組 一個正則表達式。可選參數爲需要拆分的最大數量
join()(Java8引入的) 用分隔符拼接字符片段,產生一個新的String 分隔符,待拼字符序列。用分隔符將字符序列拼接成一個新的String
substring()(即subSequence()) 返回一個新的String對象,以包含參數指定的子串 重載版本:起始索引;起始索引+終止索引
concat() 返回一個新的String對象,內容爲原始String連接上參數String 要連接的String
replace() 返回替換字符後的新String對象。如果沒有替換髮生,則返回原始的String對象 要替換的字符,用來進行替換的新字符。也可以用一個CharSequence替換另一個CharSequence
toLowerCase(),toUpperCase() 將字符的大小寫改變後,返回一個新的String對象。如果沒有任何改變,則返回原始的String對象
trim() 將String兩端的空白符刪除後,返回一個新的String對象。如果沒有任何改變,則返回原始的String對象
format() 要格式化的字符串,要替換到格式化字符串的參數 返回格式化結果String

在之後的篇幅中,作者講解了正則表達式。但也只是講了很基礎的部分(主要因爲正則表達式還是很難的,一兩句講不完),本篇的精讀不深入正則表達式。如果要專門學習,可以找相應的專門的博客,會比原著講的更細。

第十九章 類型信息

本章總字數:21000

關鍵詞:

  • RTTI
  • 類型轉換檢測
  • 反射

本章是一個過渡章節,爲下一章的泛型內容做鋪墊。爲了大家更好的理解泛型實現原理,在本章主要講了RTTI(RunTime Type Information,運行時類型信息)及反射的概念。

什麼是RTTI

我們先看一個原著的例子。
在這裏插入圖片描述

// typeinfo/Shapes.java
import java.util.stream.*;

abstract class Shape {
    void draw() { System.out.println(this + ".draw()"); }
    @Override
    public abstract String toString();
}

class Circle extends Shape {
    @Override
    public String toString() { return "Circle"; }
}

class Square extends Shape {
    @Override
    public String toString() { return "Square"; }
}

class Triangle extends Shape {
    @Override
    public String toString() { return "Triangle"; }
}

public class Shapes {
    public static void main(String[] args) {
        Stream.of(
            new Circle(), new Square(), new Triangle())
            .forEach(Shape::draw);
    }
}

結果:

Circle.draw()
Square.draw()
Triangle.draw()
  • 編譯期,stream 和 Java 泛型系統確保放入 stream 的都是 Shape 對象(Shape 子類的對象也可視爲 Shape的對象),否則編譯器會報錯;
  • 運行時,自動類型轉換確保了從 stream 中取出的對象都是 Shape 類型。

Shape 對象實際執行什麼樣的代碼,是由引用所指向的具體對象(Circle、Square 或者 Triangle)決定的。這也符合我們編寫代碼的一般需求,通常,我們希望大部分代碼儘可能少了解對象的具體類型,而是隻與對象家族中的一個通用表示打交道(本例中即爲 Shape)。這樣,代碼會更容易寫,更易讀和維護;設計也更容易實現,更易於理解和修改。所以多態是面向對象的基本目標。

所以RTTI指的是在運行時,一個引用不僅僅指向和自己類型一致的對象,還可以指向派生類對象。“使用 RTTI,我們可以查詢某個基類引用所指向對象的確切類型,然後選擇或者剔除特例。

類型轉換檢測

在Java中,RTTI會在編譯時確認對象的類型是否使用正確。如果不正確會拋出ClassCastException 異常。

RTTI在Java中還有一個使用方式: instanceof——告訴我們對象是不是某個特定類型的實例。

例如:

if(x instanceof Dog)
    ((Dog)x).bark();

在將 x 的類型轉換爲 Dog 之前,if 語句會先檢查 x 是否是 Dog 類型的對象。進行向下轉型前,如果沒有其他信息可以告訴你這個對象是什麼類型,那麼使用 instanceof 是非常重要的,否則會得到一個 ClassCastException 異常。

反射

RTTI的存在使得我們在使用類型時更加的安全。但是也存在另外一個缺陷。如果在編譯時並不知道某一個類型,RTTI就無法檢測它。

一個示例:

// typeinfo/ShowMethods.java
// 使用反射展示一個類的所有方法,甚至包括定義在基類中方法
// {java ShowMethods ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;

public class ShowMethods {
    private static String usage =
            "usage:\n" +
            "ShowMethods qualified.class.name\n" +
            "To show all methods in class or:\n" +
            "ShowMethods qualified.class.name word\n" +
            "To search for methods involving 'word'";
    private static Pattern p = Pattern.compile("\\w+\\.");

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println(usage);
            System.exit(0);
        }
        int lines = 0;
        try {
            Class<?> c = Class.forName(args[0]);
            Method[] methods = c.getMethods();
            Constructor[] ctors = c.getConstructors();
            if (args.length == 1) {
                for (Method method : methods)
                    System.out.println(
                            p.matcher(
                                    method.toString()).replaceAll(""));
                for (Constructor ctor : ctors)
                    System.out.println(
                            p.matcher(ctor.toString()).replaceAll(""));
                lines = methods.length + ctors.length;
            } else {
                for (Method method : methods)
                    if (method.toString().contains(args[1])) {
                        System.out.println(p.matcher(
                                method.toString()).replaceAll(""));
                        lines++;
                    }
                for (Constructor ctor : ctors)
                    if (ctor.toString().contains(args[1])) {
                        System.out.println(p.matcher(
                                ctor.toString()).replaceAll(""));
                        lines++;
                    }
            }
        } catch (ClassNotFoundException e) {
            System.out.println("No such class: " + e);
        }
    }
}

結果:

public static void main(String[])
public final void wait() throws InterruptedException
public final void wait(long,int) throws
InterruptedException
public final native void wait(long) throws
InterruptedException
public boolean equals(Object)
public String toString()
public native int hashCode()
public final native Class getClass()
public final native void notify()
public final native void notifyAll()
public ShowMethods()

Class 方法 getmethods() 和 getconstructors() 分別返回 Method 數組和 Constructor 數組。這些類中的每一個都有進一步的方法來解析它們所表示的方法的名稱、參數和返回值。但你也可以像這裏所做的那樣,使用 toString(),生成帶有整個方法簽名的 String。代碼的其餘部分提取命令行信息,確定特定簽名是否與目標 String(使用 indexOf())匹配,並使用正則表達式(在 Strings 一章中介紹)刪除名稱限定符。

至此,我們瞭解了兩種識別對象類型的方式:

  • “傳統的” RTTI:假定我們在編譯時已經知道了所有的類型;
  • “反射”機制:允許我們在運行時發現和使用類的信息。

瞭解這些對我們後續學習泛型至關重要。

第二十章 泛型

本章總字數:40000

關鍵詞:

  • 泛型接口
  • 泛型方法
  • 泛型擦除
  • 通配符
  • 自限定

Java的泛型概念靈感來源於C++的模版類。Java5引入泛型,起初的動機之一是爲了集合(可以參考前面集合章節)。集合比數組更靈活,而且又可以使用任何類型,這其中就有泛型的功勞。

一個簡單的泛型

當一個類要定義成泛型類,需要使用類型參數,下方示例的T 就是類型參數。

public class GenericHolder<T> {
    private T a;
    public GenericHolder() {}
    public void set(T a) { this.a = a; }
    public T get() { return a; }
    
	class Automobile {}
	
    public static void main(String[] args) {
        GenericHolder<Automobile> h3 = new GenericHolder<Automobile>();
        h3.set(new Automobile()); // 此處有類型校驗
        Automobile a = h3.get();  // 無需類型轉換
        //- h3.set("Not an Automobile"); // 報錯
        //- h3.set(1);  // 報錯
    }
}

需要說明的是,在Java7以後泛型的初始化可以簡寫:

//GenericHolder<Automobile> h3 = new GenericHolder<Automobile>();
GenericHolder<Automobile> h3 = new GenericHolder<>();
h3.set(new Automobile());

泛型接口

泛型接口是一個含有類型參數的interface,在接口內部也可以使用該類型參數。

public interface Sum<T> {
    public T getResult(T a,T b);
}

public class IntegerSum  implements Sum<Integer>
{
    @Override
    public Integer getResult(Integer a, Integer b) {
        return a+b;
    }
}
public class LongSum  implements Sum<Long>
{
    @Override
    public Long getResult(Long a, Long b) {
        return a+b;
    }
}

注意:Java中不允許多個類寫在同一個.java文件中,此處爲了方便理解僅做示例。

示例中兩個類都實現了Sum接口,但是支持的數據類型卻不一樣。而且支持類型也作用到了傳入的參數及返回值。

泛型方法

泛型接口是針對類級別的“泛化”,有時候我們需要局部的“泛化”——比如方法級別的泛型。

直接看原著示例:

// generics/GenericMethods.java

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
}

結果:

java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods

在Java中我們可以使用表示一批沒有數量限制的參數——變長參數。如下的示例中參數 num可以傳入多個也可以不傳。

 public void print(Integer... num){
 ...

使用了泛型後,依然可以這麼做:

public <T> List<T> makeList(T... args) {
...

泛型擦除

這個詞看起來有些生僻,我們可以通過一個示例先了解:

    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }

結果:

true

這個結果可能讓你有些意外,雖然兩個ArrayList泛型參數完全不同,但是Java程序認爲他們的類型一樣。

究其原因,與泛型的實現方式有關。和C++的模板類的實現不同,Java的泛型使用了泛型擦除的方式。在使用泛型時,具體的類型被擦除,這個示例中兩個ArrayList的類型都是擦除成了原始的List類型,所以纔會出現類型相同的情況。

Java 泛型是使用擦除實現的。這意味着當你在使用泛型時,任何具體的類型信息都被擦除了,你唯一知道的就是你在使用一個對象。

爲什麼Java要使用這種泛型擦除的方式實現泛型?畢竟這種方式導致了不少疑惑,比如上個示例中展現的類型判斷就是其中之一。作者在原著中用一些篇幅做了解釋,說到底與Java的向後兼容有關。

我們知道泛型是Java5才被引入的,在Java5之前的時代,Java社區中已經出現了大量的類庫,這些類庫被Java開發者廣泛使用。如果一個新特性的引入需要舊類庫被迫做出改變是不現實的,這對Java社區也是一個重大打擊——Java社區的活躍是Java之所以發展壯大的主要原因。因此,爲了妥協也或許是唯一的辦法,就是使用一種可以兼容舊版本Java又可以實現新特性的技術,這就是泛型擦除。

當某個類庫變爲泛型時,不會破壞依賴於它的代碼和應用。在確定了這個目標後,Java 設計者們和從事此問題相關工作的各個團隊決策認爲擦除是唯一可行的解決方案。擦除使得這種向泛型的遷移成爲可能,允許非泛型的代碼和泛型代碼共存。

在瞭解了泛型擦除之後,我們在使用泛型時要時刻注意。

比如:

class Foo<T> {
    T var;
}
public class cat {}
...
Foo<Cat> f = new Foo<>();

當你聲明一個 Foo< Cat > 時,一定要明白,你只是聲明瞭一個Object:

Foo<Object> f = new Foo<>();

擦除引出的類型判斷問題,可以使用一些方法來解決,其中之一是使用isInstance。

public class ClassTypeCapture<T> {
    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
}

由於擦除刪除了類型信息,如果在泛型類型中需要使用限定性的動作,就必須有類型的邊界限制。通俗的講,有一個動物的泛型類,需要傳入的T都是具有“吃”和“睡”的行爲,如果我們對T不加限制,那在調用這些行爲時就有不可預知的bug。

在Java泛型中我們可以使用extends 關鍵詞來限制邊界。

class Bounded extends Coord implements HasColo{
...
}
class WithColorCoord<T extends Coord & HasColor> {
...
}

通配符

在本章作者用了大篇幅的範例,但是內容不那麼容易理解。所以這裏我重新舉例來講講。

extends super 關鍵詞可以限制泛型類型的邊界。

看一個例子:

	public class Animal {}
    public class Cat extends Animal {}
    public class BlackCat extends Cat {}

    public class House<T> {
        private T item;
        public House(T t) {
            item = t;
        }
        public void set(T t) {
            item = t;
        }
        public T get() {
            return item;
        }
    }

我們看到了Cat繼承自Animal,而BlackCat繼承自Cat類。我們在使用泛型類House時,出現了一個問題:

Cat c1=new BlackCat();//OK
House<Cat> h2=new House<BlackCat>(new BlackCat());//Error

很顯然,雖然黑貓也是貓,但是貓住的窩,黑貓卻不能住。這讓人感到疑惑。很顯然編譯器沒有我們想象的那麼聰明,它遇到了類型判斷上的困難,在不確定類型關係的前提下給了錯誤提示。爲了解決這個問題,Java設計者提出了通配符來控制類型上下界關係。

  • <? extends T>:上界通配符(Upper Bounds Wildcards)
  • <? super T>:下界通配符(Lower Bounds Wildcards)

上界代表:凡是指定類型或者指定類型的派生類
下界代表:凡是指定類型或者指定類型的任何基類

這樣我們就可以嚴格控制泛型類型的邊界,並且告訴編譯器我們的聲明類型之間的聯繫。

House<? extends Cat> h2=new House<BlackCat>(new BlackCat());//OK
House<? super Cat> h3=new House<Animal>(new BlackCat());//OK

翻譯一下:
凡是Cat的派生類或者Cat本身可以被聲明。因爲BlackCat是Cat的派生類,所以可以聲明。
凡是Cat的基類或者Cat本身可以被聲明。因爲Animal是Cat的基類,所以可以被聲明。

讓我們使用通配符來聲明,並且嘗試爲其賦值:

House<? extends Cat> house = new House<BlackCat>(new BlackCat());
house.set(new Animal());//Error
house.set(new Cat());//Error
house.set(new BlackCat());//Error
Animal animal1 = house.get();
Cat cat1 = house.get();
BlackCat blackCat1 = house.get();//Error
Object animal2 = house.get();

House<? super Cat> house2 = new House<Animal>(new Cat());
house2.set(new Animal());//Error
house2.set(new Cat());
house2.set(new BlackCat());
Animal animal3 = house2.get();//Error
Cat cat2 = house2.get();//Error
Object animal4 = house2.get();

結果與我們預期有很大差異。這其中有不少代碼被編譯器認爲是錯誤的。這就涉及到了上下界的侷限性。

因爲 set() 的參數也是"? extends Fruit",意味着它可以是任何事物,編譯器無法驗證“任何事物”的類型安全性.

在爲上界賦值時,由於編譯器無法確定其參數類型到底是指定類的哪一種派生類,出於類型安全性會給予錯誤提示。但是獲取值時,編譯器可以確定其返回類型(返回值一定是一隻貓且一定是一隻動物)。

相同的道理,在下界中get()方法的返回值由於編譯器無法確定其類型到底是那一種,所以只能接受所有類的基類——Object類型。

自限定

自限定是個有趣的概念。在定義一個泛型類時我們對泛型參數的限定可以作用於這個類本身。

class SelfBounded<T extends SelfBounded<T>> { // ...

這個語句的意思是,這個類中的泛型參數T 必須是繼承自這個類(也就是它的派生類)。

自限定在某些情景下會有奇效,比如用來限制方法參數只能用特定類型:

class SelfBounded<T extends SelfBounded<T>> {
    T element;
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() { return element; }
}
...
static <T extends SelfBounded<T>> T f(T arg) {
	return arg.set(arg).get();
}

總結

本篇的主要篇幅在泛型部分,特別是泛型擦除。原著中關於泛型的講解着墨更多。如果對泛型的知識想進一步瞭解,建議讀一遍原文章節。本篇的字符串部分屬於基礎但是很重要的部分,需要細細瞭解,因爲經常會用到。本篇耗時一週,整個五一陪伴度過。 TT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章