Java基礎系列—字串

牛覓發表於2018-03-13

可以證明,字串操作是計算機程式設計中最常見的行為。

不可變String

String物件是不可變的。檢視JDK文件你就會發現,String類中每一個看起來會修改String值的方法,實際上都是建立了一個全新的String物件,以包含修改後的字串內容,而最初的String物件則絲毫未變。

public class Immutable {

    public static String upcase(String s){
        return  s.toUpperCase();
    }

    public static void main(String[] args) {
        String q = "howdy";
        System.out.println(q);
        String qq = upcase(q);
        System.out.println(qq);
        System.out.println(q);
    }
}
複製程式碼

過載 “+” 與StringBuilder

不可變性會帶來一定的效率問題。用於String的“+”與“+=”是Java中僅有的兩個過載過的操作符,而Java並不允許程過載任何操作符。

操作符 “+” 可以用來連線String:

public class Concatenation {

    public static void main(String[] args) {
        String mango = "mango";
        String s = "abc" + mango;
        System.out.println(s);
    }
}
複製程式碼

想看看以上程式碼到底是如何工作的嗎,可以用JDK自帶的工具javap來反編譯以上程式碼,可以得到以下的位元組碼:

位元組碼

編譯器自動引入了java.lang.StringBuilder類(因為它更高效),通過呼叫append方法將字串連線起來,最後呼叫toString方法生成最終結果。

避免迴圈體內使用 "+="

public String implicit(String[] fields) {
    String result = "";
    for (int i = 0; i < fields.length; i++) {
        result += fields[i];
    }
    return result;
}
複製程式碼

通過反編譯得如下位元組碼:

Jietu20180311-174853.jpg

StringBuilder是在迴圈體內構造的,這意味著每經過迴圈一次,就會建立一個新的StringBuilder物件。

無意識的遞迴

public class InfiniteRecursion {

    @Override
    public String toString() {
        // 列印InfiniteRecursion物件的記憶體地址
        return " InfiniteRecursion adress: " + this + "\n";
    }

    public static void main(String[] args) {
        InfiniteRecursion infiniteRecursion  = new InfiniteRecursion();
        System.out.println(infiniteRecursion);
    }
}
複製程式碼

執行以上程式出現如下結果:

執行結果

" InfiniteRecursion adress: " + this這裡發生了自動型別轉換,由InfiniteRecursion型別轉換成String型別。因為編譯器發現String物件後面跟著一個 “+”,而後面的物件不是String,編譯器呼叫this.toString()方法進行型別轉換,因此發生了遞迴呼叫。

如果你真的想要列印出物件的記憶體地址,應該呼叫Object.toString()方法。所以不該使用this,而是應該呼叫super.toString()方法。

格式化輸出

Java SE5推出了C語言中printf()風格的格式化輸出這一功能,不需要使用過載的 “+”操作符來連線引用號內的字串或者字串常量,而是使用特殊的佔位符來表示資料將來的位置。

public static void main(String[] args) {
    int x = 5;
    double y = 5.332;

    // the old way
    System.out.println("Row 1:[" + x + " " + y + "]");

    // the new way
    System.out.printf("Row 1:[%d %f]\n", x, y);
    
    // or
    System.out.format("Row 1:[%d %f]\n", x, y);
}
複製程式碼

執行以上程式,首先將x的值插入到%d的位置,然後將y的值插入%f的位置。這些佔位符被稱為格式修飾符,它們不但說明了將插入什麼型別的變數,以及如何對其格式化。

Formatter類

在Java中,所有新的格式化功能都由java.util.Formatter類處理。可以將Formatter類看作一個翻譯器,它將你的格式化字串與資料翻譯成需要的結果。

Formatter formatter = new Formatter(System.out);
formatter.format("Row 1:[%d %f]\n", x, y);
複製程式碼

String.format()

String.format()是一個static方法,它接受與Formatter.format()方法一樣的引數,但返回一個String物件。

格式化說明符

在插入資料時,如果想要控制空格與對齊,你需要更精細複雜的格式修飾符。以下是其抽象的語法:

%[argument_index$][flags][width][.precision]conversion
複製程式碼
欄位 說明
argument_index 需要將引數列表中第幾個引數進行格式化
flags 一些特殊的格式,比如‘-’代表向左對齊
width 輸出的最小的寬度,適用於所有的引數型別
[.precision] 引數為String,則表示列印String輸出字元的最大數量;引數為float,則表示小數點最大位數。不使用於int
conversion 接受的引數型別,如s代表後面接String型別的引數;d代表接int型的引數

引數詳細說明

正規表示式

正規表示式是一種強大而靈活的文字處理工具。使用正規表示式,我們能夠以程式設計的方式,構造複雜的文字模式,並對輸入的字串進行搜尋。一旦找到了匹配這些模式的部分,你就能夠隨心所欲地對它們進行處理。

Java語言與其他語言相比對反斜槓\有不同的處理

在其他語言中,\\表示“我想要在正規表示式中插入一個普通的(字面上的)反斜槓,請不要給它做任何特殊的意義。” 而在Java中,\\的意思是“我要插入一個正規表示式的反斜槓,所以其後的字元具有特殊的意義。”

例如,如果你想表示一位數字,那麼正規表示式應該是\\d。如果你想插入一個普通的反斜槓,則應該這樣\\\\。不過換行和製表符之類的東西只需使用單斜槓線:\n\t

String類支援正規表示式的方法

public String[] split(String regex)

public String[] split(String regex, int limit) 

public String replaceFirst(String regex, String replacement)

public String replaceAll(String regex, String replacement) 

public boolean matches(String regex) 
複製程式碼

Pattern和Matcher

一般來說,比起功能有限的String類,我們更願意構造功能強大的正規表示式物件。通過Pattern.complie()方法來編譯你的正規表示式即可。它會根據你的String型別的正規表示式生成一個Pattern物件。接下來,把你想要檢索的字串傳入Pattern物件的matcher()方法,matcher()方法會生成一個Matcher物件,它有很多功能可用。

public class RegexExpression {

    public static void main(String[] args) {
        String phone = "18926119073";
        Pattern pattern = Pattern.compile("1([358][0-9]|4[579]|66
                |7[0135678]|9[89])[0-9]{8}");
        Matcher matcher = pattern.matcher(phone);
        System.out.println(matcher.matches());
    }
}
複製程式碼

組是用括號劃分的正規表示式,可以根據組的編號來引用某個組。組號為0表示整個表示式,組號1表示被第一對括號括起的組,依次類推。因此,在下面這個表示式:

A(B(C))D
複製程式碼

中有三個組:組0是ABCD,組1是BC,組2是C。

Mather提供了很多有用的方法具體使用檢視API,使用Mather的替換方法可以實現隱藏手機號中間數字、隱藏使用者名稱等。

String phone = "18926119073";
Pattern pattern = Pattern.compile("(\\d{3})\\d{4}(\\d{4})");
Matcher matcher = pattern.matcher(phone);
phone = matcher.replaceAll("$1****$2");
System.out.println(phone);
複製程式碼

用正規表示式掃描

Java SE5新增類Scanner類,它可以大大減輕掃描輸入的工作。

public class ScannerRead {

    public static void main(String[] args) {
        Scanner scanner = new Scanner("Sir Robin of Camelot
        \n22 1.61803"));
        System.out.println("What is your name?");
        String name = scanner.nextLine();
        System.out.println(name);
        System.out.println("(input: <age> <double>)");
        System.out.println(scanner.nextInt()+" "+ scanner.nextDouble());
    }
}
複製程式碼

Scanner的構造期可以接受任何型別的輸入物件,包括File物件、InputStream、String或者Readable物件。

Scanner所有的輸入、分詞以及翻譯的操作都隱藏在不同型別的next方法中,普通的next()方法返回下一個String,所有的基本型別(除char之外)都有對應的next方法,包括BigDecimal和BigInteger。所有的next方法,只有在找到一個完整的分詞之後才會返回,hasNext方法用以判斷下一個輸入分詞是否所需的型別。

在預設的情況下,Scanner根據空白符對輸入進行分詞,但是你可以用正規表示式指定自己所需的定界符:

public class ScannerDelimiter {

    public static void main(String[] args) {
        Scanner scanner = new Scanner("12,42,78,99");
        scanner.useDelimiter(",");
        while (scanner.hasNextInt()){
            System.out.println(scanner.nextInt());
        }
    }
}
複製程式碼

除了能夠掃描基本型別之外,你還可以使用自定義的正規表示式進行掃描。如下所示:

public class ThreatAnalyzer {

    static String threatData =
            "58.27.82.161@02/10/2005\n" +
            "124.45.82.161@02/10/2005\n" +
            "58.27.82.161@02/10/2005\n" +
            "72.27.82.161@02/10/2005\n" +
             "[Next log section with different data format]";

    public static void main(String[] args) {
        Scanner scanner = new Scanner(threatData);
        String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})";
        while (scanner.hasNext(pattern)) {
            scanner.next(pattern);
            MatchResult match = scanner.match();
            String ip = match.group(1);
            String date = match.group(2);
            System.out.format("Threat on %s from %s\n", date, ip);
        }
    }
}
複製程式碼

相關文章