facebook廣告





123

2016年10月22日 星期六

常見Map的特色及用法

來源:http://whu241.blogspot.tw/2013/12/map.html


Map又稱關聯式陣列(Associative Array),為一種使用key-value pair的方式來存取資料的資料結構。在Java Collection Framework中,java.util.Map只是一個Interface,當中定義了一個map所該具有的基本操作;而實作Map介面的類別至少就超過了15個,從這我們也可看出map的重要性了!


不同的實作,由於其針對的面向不一樣,其使用效率及能解決的問題就各自不同,以下筆者描述一些較常見的map class其特點及用法。


HashMap

為以Hash Table為base所發展出來的類別,基本上在使用map時,若無其它考量,則我們應該優先使用HashMap,因其存取資料的時間複雜度可以達到常數時間,非常地快。另外我們也能在HashMap的建構子設定其Capacity,及loading factor。 若我們使用自訂物件作為HashMap的Key,則此時一定要注意是否正確地覆寫了equals()及hashcode(),否則在使用以雜湊函式為基底的類別及函式時,很可能會出現非開發者預期的結果。另外較特別的是HashMap允許鍵值(key)為null喔。



建構函式

[code lang="java"]


//無引數建構子,表採用預設的capacity(16),及load factor(0.75)

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


//指定capacity為32,load factor仍為預設的0.75

Map<String, String> hm1 = new HashMap<String, String> (32);


//指定capacity為32,load factor為0.8

Map<String, String> hm2 = new HashMap<String, String> (32,(float) 0.8);

[/code]


不保證順序 

[code lang="java"]

public class MapSorting {

public static void main(String[] args) {

//iterate時,不保證順序

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

hm.put("J", "John");

hm.put("M", "Mary");

hm.put("B", "Bill");

hm.put("C", "Christine");

hm.put("A", "Ariel");


printMap(hm);

}


public static void printMap(Map<String,String> map) {

for (Map.Entry<String,String> entry:map.entrySet()) {

System.out.println(entry.getKey()+" "+entry.getValue());

}

}

}

[/code]


Output從輸出結果中我們可以看到,hashMap輸出key時,並沒有依照插入時的順序,也沒有依照Natural Ordering(M在J前面),所以我們若需要有排序功能的map時,不能選擇HashMap。


[code lang="text"]

A Ariel

B Bill

C Christine

M Mary

J John

[/code]


為了精簡起見,以下的程式碼例子便不再列出main()及printMap函式。


LinkedHashMap

見文生義,LinkedHashMap內部是用linked list來維護其順序性,所以在iterate時其結果乃是依照元素的插入順序或最近最少使用(least-recently-used)順序。在使用上其與hashmap相似,速度只稍差些;但在iterate時卻是比hashmap還來得快喔^^ 而實務上我們也常用其來實作LRU Cache。


[code lang="java"]


//iterate時,保證其順序為插入順序或最近最少使用(least-recently-used,LRU)的順序

Map<String, String> lhm = new LinkedHashMap<String, String> ();

lhm.put("J", "John");

lhm.put("M", "Mary");

lhm.put("B", "Bill");

lhm.put("C", "Christine");

lhm.put("A", "Ariel");


printMap(lhm);


[/code]


Output

我們可以看到輸出結果與元素插入時的順序一致。


[code lang="text"]

J John

M Mary

B Bill

C Christine

A Ariel

[/code]


TreeMap

紅黑樹(red-black tree)的一個實作品,其特點是其key set或key-value pair是有順序性的,而順序為natual ordering或是由所傳入的comparator來決定。另外TreeMap也是唯一一個提供submap()函式的map。


[code lang="java"]

//iterate時,保證其順序為Natural Ordering或Comparator來決定

Map<String, String> tm = new TreeMap<String, String> ();

tm.put("J", "John");

tm.put("M", "Mary");

tm.put("B", "Bill");

tm.put("C", "Christine");

tm.put("A", "Ariel");


printMap(tm);


System.out.println("----------by comparator");

//sort by comparator

tm = new TreeMap<String,String> (new Comparator<String>() {

public int compare(String o1, String o2) {

return o2.compareTo(o1);

}

});

tm.put("J", "John");

tm.put("M", "Mary");

tm.put("B", "Bill");

tm.put("C", "Christine");

tm.put("A", "Ariel");


printMap(tm);


[/code]


Output


[code lang="plain"]

A Ariel

B Bill

C Christine

J John

M Mary

----------by comparator

M Mary

J John

C Christine

B Bill

A Ariel

[/code]



EnumMap

也為Map的一個實作,其特別之處在於只接受列舉(Enumeration)為Key,也因其只接受列舉為key,不像HashMap能接受各種型態的物件作為key,故在實作上能特地為此種情況最佳化。


EnumMap的好處可以從效率上及使用上來描述:技術上,由於EnumMap內部使用Array來實作;另外因不需用呼叫hashcode函式,故其也不會產生collision的問題;所以在同是key為enum的情況下,EnumMap的效能是好過HashMap的。而在使用上,以列舉作為key便不怕有打錯字的情況了,這個特性,筆者非常地喜歡!



特色:

1. 不接受null為key。

2. 以Natural Ordering的方式來儲存Key。

3. 效能比HashMap稍好些。


[code lang="java"]


import java.util.EnumMap;

import java.util.Map;


enum Keys {

A(1), B(2), C(3), J(4), M(5);


private int code;


private Keys(int code) {

this.code = code;

}


public int getCode() {

return this.code;

}

}


ublic class EnumTest {

public static void main(String[] args) {

Map<Keys, String> enumMap = new EnumMap<Keys, String> (Keys.class);

enumMap.put(Keys.J, "John");

enumMap.put(Keys.M, "Mary");

enumMap.put(Keys.B, "Bill");

enumMap.put(Keys.C, "Christine");

enumMap.put(Keys.A, "Ariel");




for (Map.Entry<Keys, String> entry: enumMap.entrySet()) {

System.out.println(entry.getKey()+" "+entry.getKey().getCode()+" "+entry.getValue());

}

}

}


[/code]


Output
我們可看到輸出的順序為key按照Natural Ordering的排序。


[code lang="plain"]

A 1 Ariel

B 2 Bill

C 3 Christine

J 4 John

M 5 Mary

[/code]


WeakHashMap

WeakHashMap在設計上使用canonicalized mappings來節省儲存空間,而其也能讓GC自動地回收key-value pair,讓使用者不用自行清理。 一般而言,一個物件若有reference指向它時,其是不會被GC回收掉的。如hashMap.put("a",Object A),由於key "a"指向Object A,所以就算key a己沒有被其它程式使用到,key "a"及其value仍不會被回收掉,開發者需手動呼叫remove(),才能避免空間的浪費。而WeakHashMap中的的key若沒被其它程式reference時,這對key-value pair便會自動被GC回收掉。


[code lang="java"]

import java.util.HashMap;

import java.util.Map;

import java.util.WeakHashMap;


public class WeakHashMapTest {

public static void main(String[] args) {

String hashKey = new String("5566");

String weakHashKey = new String("8899");


ap<String,String> hashMap = new HashMap<String,String>();

Map<String,String> weakHashMap = new WeakHashMap<String,String>();



hashMap.put(hashKey, "I'm sad!");

weakHashMap.put(weakHashKey, "Apple");


System.out.printf("value in HashMap: %s \n",hashMap.get(hashKey));

System.out.printf("value in WeakHashMap: %s \n",weakHashMap.get(weakHashKey));


//將兩個map的鍵值設為null

hashKey = null;

weakHashKey = null;


//建議啟動Garbage Collection

System.gc();


System.out.println("-----after Garbage Collection------");

System.out.printf(" HashMap--> size: %d , ",hashMap.size());

for (String str:hashMap.values()){

System.out.printf("value: %s \n",str);

}


System.out.printf("WeakHashMap--> size: %d , ",weakHashMap.size());

for (String str:weakHashMap.values()) {

System.out.printf(" value: %s \n", str); //key-value entry己被清掉,迴圈己進不來了

}

}

}

[/code]




Output


[code lang="plain"]

value in HashMap: I'm sad!

value in WeakHashMap: Apple

-----after Garbage Collection------

HashMap--> size: 1 , value: I'm sad!

WeakHashMap--> size: 0 ,

[/code]



ConcurrentHashMap
雖然HashMap很好用,但是其並非thread-safe的。當有多個執行緒同時對HashMap進行讀取及修改的動作時,便可能產生「ConcurrentModificationException」;而在JDK 1.5之前,只有HashTable及利用Collections.synchronizedMap()才能保証map在多執行緒環境下的安全,但這兩個方式採用的方法為鎖住整個map,這會造成效能的顯著下降。所以在Sun在1.5時加入了ConcurrentHashMap類別,它能保證thread-safe且效能也不錯。嗯這是怎麼做到的呢?讓我們繼續看下去^^ (為了方便,以下用chm來簡稱ConcurrentHashMap)


為了讓同一個map可以被很多個執行緒同時又讀又寫,chm的作法是只鎖住部份的map!其藉由使用ConcurrencyLevel的值(可在建構子中指定,預設值為16)來將map切成很多塊,。每一塊皆能由不同的執行緒來使用,所以使用不同塊的執行緒並不會互相干擾,而用到同一塊map的執行緒們的溝通則還是藉由lock的機制來保護。



特色

1. 不會丟出ConcurrentModificationException.

2. 有個特別的putIfAbsent(key, value)函式。

3. 不允許鍵值為null。




多執行緒同時存取HashMap

[code lang="java"]

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;



public class NotThreadSafeMap implements Runnable {

private String name;

private static Map<Integer, String> map = new HashMap<Integer,String>();



public NotThreadSafeMap(Integer number, String name) {

this.name = name;

map.put(number, name);

}



public void run() {

try{

Iterator<Integer> it = map.keySet().iterator();

while(it.hasNext()) {

Integer key = it.next();

map.put(key+1, name);

}

System.out.println(name+ " inserted");

} catch(Exception e) {

e.printStackTrace();

} finally {

}

}



public static void main(String[] args) {

NotThreadSafeMap not1 = new NotThreadSafeMap(1,"Apple");

NotThreadSafeMap not2 = new NotThreadSafeMap(2,"Beagle");



ExecutorService executor = Executors.newCachedThreadPool();

executor.execute(not1);

executor.execute(not2);



for (Entry<Integer, String> entry: map.entrySet()) {

System.out.println("Key:" + entry.getKey() + " Value:" + entry.getValue());

}

executor.shutdownNow();

}

}

[/code]



Output
產生了exception@@



[code lang="plain"]

Key:1 Value:Apple

Beagle inserted

Apple inserted

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.HashMap$HashIterator.nextEntry(HashMap.java:894)

at java.util.HashMap$EntryIterator.next(HashMap.java:934)

at java.util.HashMap$EntryIterator.next(HashMap.java:932)

at fun.practice.map.NotThreadSafeMap.main(NotThreadSafeMap.java:41)

[/code]



多執行緒同時存取CHM



[code lang="java"]

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;



public class ThreadSafeMap implements Runnable {

private String name;

private static Map<Integer, String> map = new ConcurrentHashMap<Integer,String>();



public ThreadSafeMap(Integer number, String name) {

this.name = name;

map.put(number, name);

}



public void run() {

try{

Iterator<Integer> it = map.keySet().iterator();

while(it.hasNext()) {

Integer key = it.next();

map.put(key+1, name);

}

System.out.println(name+ " inserted");

} catch(Exception e) {

e.printStackTrace();

} finally{

}

}



public static void main(String[] args) {

ThreadSafeMap not1 = new ThreadSafeMap(1,"Apple");

ThreadSafeMap not2 = new ThreadSafeMap(2,"Beagle");



ExecutorService executor = Executors.newCachedThreadPool();

executor.execute(not1);

executor.execute(not2);



for (Entry<Integer, String> entry: map.entrySet()) {

System.out.println("Key:" + entry.getKey() + " Value:" + entry.getValue());

}

executor.shutdownNow();

}

}

[/code]



Output

結果正常,不會產生Exception。



[code lang="lang"]

Apple inserted

Key:2 Value:Apple

Key:1 Value:Apple

Key:3 Value:Beagle

Key:4 Value:Beagle

Beagle inserted

[/code]



IdentityHashMap

HashMap在判斷一個鍵值(key)是否己存在時,會呼叫key物件的equals()方法來辨別,任何物件預設的equals()方法都是用物件預設的reference來比較,被覆寫後就是依照此物件的邏輯來比了。IdentityHashMap則是使用"==",也就是利用reference來比較兩個物件是否相等,不管此物件的equals()有無被覆寫;而其是利用System.identityHashCode(object)來產生hashcode,所以也不會受到mutable object的影響。


IdentityHashMap之所以會存在主要是為了解決使用可變物件(mutable object)為key時,hashmap可能會遇到的困擾。如下例中筆者自行定義了一個Person物件,有name及age兩個屬性。另外筆者覆寫了equals(),以name及age來判斷兩個person物件是否一樣;程式一開始用hashmap來存放person與職稱的對應,在放入了一個person物件為key後,我們改變了此person物件的age,由於age會被equals()使用到,當呼叫containsKey()時其結果會變得不可預測,另外此例中age也會影響到hashcode的計算,所以當拿改變過後的person物件,想取出對應的職稱時,hashmap也可能會找不到原本其應該mapping到的value。


而若我們使用IdentityHashMap來存mutable object時,不管此物件在被放入map之後經過了多少的改變,由於是使用reference來判斷key是否相等,所以containsKey()傳回來的結果總是一致的,另外在getValue(key)時,其傳回的hashCode也不會收到物件改變的影響。



[code lang="java"]



import java.util.HashMap;

import java.util.IdentityHashMap;

import java.util.Map;

import static java.lang.System.out;



class Person {

private String name;

private Integer age;



public Person (String name,Integer age) {

this.name = name;

this.age = age;

}



public void setName(String name) {

this.name = name;

}



public void setAge(Integer age) {

this.age = age;

}



@Override

public boolean equals(Object obj) {

if (obj instanceof Person) {

Person person2 = (Person) obj;

if (this.name.equals(person2.name) && this.age == person2.age) {

return true;

}

}




return false;

}



@Override

public int hashCode() {

return this.age * 47;

}



}



public class IdentityMapTest {

public static void main(String... args) {

Person p = new Person("Wallace",31);



out.println("Mutable object in Hashmap:");



Map<Person, String> hashMap = new HashMap<Person, String>();

hashMap.put(p, "Engineer");



out.println("titleBeforeChange:"+hashMap.get(p));

p.setAge(99);

out.println("title1AfterChange:"+hashMap.get(p));



out.println("\nMutable object in IdentityHashmap:");

p = new Person("Wallace",31);



Map<Person, String> iHashMap = new IdentityHashMap<Person, String>();

iHashMap.put(p, "Engineer");




out.println("titleBeforeChange:"+iHashMap.get(p));

p.setAge(99);

out.println("title1AfterChange:"+iHashMap.get(p));



}

}



[/code]



Output

我們可以看到第3行的取出結果為null,這便是問題所在了。



[code lang="plain"]



Mutable object in Hashmap:

titleBeforeChange:Engineer

title1AfterChange:null



Mutable object in IdentityHashmap:

titleBeforeChange:Engineer

title1AfterChange:Engineer



[/code]



Reference

1. Thinking In Java, 4e.

2. http://java.dzone.com/articles/difference-between-hashmap-and

2016年10月5日 星期三

将String转换成InputStream

String   str   =   "";//add   your   string   content
InputStream   inputStream   =   new   ByteArrayInputStream(str.getBytes());


2016年10月2日 星期日

Java Gossip: 實作 Runnable 介面

一個進程(Process)是一個包括有自身執行位址的程式,在一個多工的作業系統中,可以分配CPU時間給每一個進程,CPU在片段時間中執行某個進程,然後下一個時間片段跳至另一個進程去執行,由於轉換速度很快,這使得每個程式像是在同時進行處理一般。

一個執行緒是進程中的一個執行流程,一個進程中可以同時包括多個執行緒,也就是說一個程式中同時可能進行多個不同的子流程,這使得一個程式可以像是同時間 處理多個事務,例如一方面接受網路上的資料,另一方面同時計算資料並顯示結果,一個多執行緒程式可以同時間處理多個子流程。

在Java中要實現執行緒功能,可以實作Runnable介面,Runnable介面中只定義一個run()方法,然後實例化一個 Thread物件時,傳入一個實作Runnable介面的物件作為引數,Thread物件會調用Runnable物件的run()方法,進而執行當中所定義的流程。

下面這個程式是個簡單的Swing程式,您可以看到如何實作Runnable介面及如何啟動執行緒:
 
  • ThreadDemo.java
package onlyfun.caterpillar;
 
import javax.swing.*;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.event.*;
 
public class ThreadDemo extends JFrame {
    public ThreadDemo() {
        // 配置按鈕 
        JButton btn = new JButton("Click me"); 
        
        // 按下按鈕後繪製圓圈 
        btn.addActionListener(new ActionListener() { 
            public void actionPerformed(ActionEvent e) {
                Thread thread1 = new Thread(new Runnable() {
                    public void run() {
                        Graphics g = getGraphics(); 

                        for(int i = 10; i < 300; i+=10) { 
                            try { 
                                Thread.sleep(500); 
                                g.drawOval(i, 100, 10, 10); 
                            } 
                            catch(InterruptedException e) { 
                                e.printStackTrace(); 
                            } 
                        }
                    }
                });
                
                Thread thread2 = new Thread(new Runnable() {
                    public void run() {
                        Graphics g = getGraphics(); 

                        for(int i = 10; i < 300; i+=10) { 
                            try { 
                                Thread.sleep(500); 
                                g.drawOval(i, 150, 15, 15); 
                            } 
                            catch(InterruptedException e) { 
                                e.printStackTrace(); 
                            } 
                        }
                    }
                });
                
                thread1.start();
                thread2.start();
            } 
        }); 
        
        getContentPane().add(btn, BorderLayout.NORTH); 

        // 取消按下視窗關閉鈕預設動作 
        setDefaultCloseOperation(
                   WindowConstants.EXIT_ON_CLOSE); 
        setSize(320, 200);
        
        setVisible(true);
    }

    public static void main(String[] args) {
        new ThreadDemo();
    }
}

將程式編譯並執行時,您可以看到一個視窗,按下上面的按鈕,您會看到兩個圓在「同時」繪製,雖說是同時,其實也只是錯覺而已,其實是CPU往來兩個流程之間不斷的進行繪製圓的動作而已。

Thread類別也實作了Runnable介面,您也可以繼承Thread類別並重新定義它的run()方法,好處是可以使用Thread上的一些繼承下來的方法,例如yield(),然而繼承了Thread就表示您不能讓您的類別再繼承其它的類別。


引用:http://openhome.cc/Gossip/JavaGossip-V2/RunnableInterface.htm

#Android#OkHttp3使用指南

#Android#OkHttp3使用指南

知识框架(脑图)


Okhttp3脑图

出现背景

网络访问的高效性要求,可以说是为高效而生

解决思路

  1. 提供了对 HTTP/2 和 SPDY 的支持,这使得对同一个主机发出的所有请求都可以共享相同的套接字连接
  2. 如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率
  3. 提供了对 GZIP 的默认支持来降低传输内容的大小
  4. 提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求
  5. 当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址

OkHttp3设计思路

具体步骤

(1)添加网络访问权限并添加库依赖
<uses-permission android:name="android.permission.INTERNET" />
compile 'com.squareup.okhttp3:okhttp:3.4.1'
(2)GET
OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}
(3)POST
public static final MediaType JSON
    = MediaType.parse("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(JSON, json);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  Response response = client.newCall(request).execute();
  return response.body().string();
}
(3)异步调用
使用enqueue方法,将call放入请求队列,然后okHttp会在线程池中进行网络访问;只需要在适当的时候(需要操作UI的时候)发送一个消息给主线程的Handler(取决于Looper,使用Looper.getMainLooper()创建的Handler就是主线程Handler)就可以了~
private Handler mHandler;
private TextView mTxt;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);
    mTxt = (TextView) findViewById(R.id.txt);
    mHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            mTxt.setText((String) msg.obj);
        }
    };
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder().url("https://github.com").build();
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
        }
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            Message msg = new Message();
            msg.what=0;
            msg.obj = response.body().string();
            mHandler.sendMessage(msg);
        }
    });
}
(4)HTTP头部的设置和读取
HTTP 头的数据结构是 Map<String, List<String>>类型。也就是说,对于每个 HTTP 头,可能有多个值。但是大部分 HTTP 头都只有一个值,只有少部分 HTTP 头允许多个值。OkHttp的处理方式是:
  • 使用header(name,value)来设置HTTP头的唯一值
  • 使用addHeader(name,value)来补充新值
  • 使用header(name)读取唯一值或多个值的最后一个值
  • 使用headers(name)获取所有值
OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
        .url("https://github.com")
        .header("User-Agent", "My super agent")
        .addHeader("Accept", "text/html")
        .build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
    throw new IOException("服务器端错误: " + response);
}

System.out.println(response.header("Server"));
System.out.println(response.headers("Set-Cookie"));
(5)表单提交
RequestBody formBody = new FormEncodingBuilder()
            .add("query", "Hello")
            .build();
(6)文件上传
使用MultipartBuilder指定MultipartBuilder.FORM类型并通过addPart方法添加不同的Part(每个Part由Header和RequestBody两部分组成),最后调用builde()方法构建一个RequestBody。
MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");
RequestBody requestBody = new MultipartBuilder()
    .type(MultipartBuilder.FORM)
    .addPart(
            Headers.of("Content-Disposition", "form-data; name=\"title\""),
            RequestBody.create(null, "测试文档"))
    .addPart(
            Headers.of("Content-Disposition", "form-data; name=\"file\""),
            RequestBody.create(MEDIA_TYPE_TEXT, new File("input.txt")))
    .build();
(7)使用流的方式发送POST请求
OkHttpClient client = new OkHttpClient();
final MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain");
final String postBody = "Hello World";

RequestBody requestBody = new RequestBody() {
    @Override
    public MediaType contentType() {
        return MEDIA_TYPE_TEXT;
    }
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8(postBody);
    }
    @Override
    public long contentLength() throws IOException {
        return postBody.length();
    }
};

Request request = new Request.Builder()
        .url("http://www.baidu.com")
        .post(requestBody)
        .build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
    throw new IOException("服务器端错误: " + response);
}
System.out.println(response.body().string());
(8)缓存控制
强制不缓存,关键:noCache()
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder().noCache().build())
    .url("http://publicobject.com/helloworld.txt")
    .build();
缓存策略由服务器指定,关键:maxAge(0, TimeUnit.SECONDS)
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder()
        .maxAge(0, TimeUnit.SECONDS)
        .build())
    .url("http://publicobject.com/helloworld.txt")
    .build();
强制缓存,关键:onlyIfCached()
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder()
        .onlyIfCached()
        .build())
    .url("http://publicobject.com/helloworld.txt")
    .build();
Response forceCacheResponse = client.newCall(request).execute();
if (forceCacheResponse.code() != 504) {
  // The resource was cached! Show it.
} else {
  // The resource was not cached.
}
允许使用旧的缓存,关键:maxStale(365, TimeUnit.DAYS)
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder()
        .maxStale(365, TimeUnit.DAYS)
        .build())
    .url("http://publicobject.com/helloworld.txt")
    .build();

Q&A

问题1:CalledFromWrongThreadException怎么破?

分析:从错误的线程调用,是因为在主线程中操作UI,这在Android中是不允许的,所以需要切换到主线程中进行UI操作。
解决:参见 (6)异步调用

问题2:Cookies没有被缓存怎么破?

分析:Cookies由CookieJar统一管理,所以只需要对CookieJar进行设置就可以达到目的了。
解决:
OkHttpClient mHttpClient = new OkHttpClient.Builder().cookieJar(new CookieJar() {
    private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
    //Tip:key是String类型且为url的host部分
    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        cookieStore.put(url.host(), cookies);
    }
    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        List<Cookie> cookies = cookieStore.get(url.host());
        return cookies != null ? cookies : new ArrayList<Cookie>();
    }
}).build();

问题3:如何实现Cookies持久化?

方案1:使用PersistentCookieJar
在Project的Build.gradle中添加Maven库
allprojects {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}
在引入依赖库
compile 'com.github.franmontiel:PersistentCookieJar:v0.9.3'
创建并使用PersistentCookieJar
ClearableCookieJar cookieJar =
                new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(context));

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cookieJar(cookieJar)
                .build();
方案2:参考android-async-http库写一个
参考两个类,一个是 PersistentCookieStore.java,另一个是 SerializableCookie.java。参见参考文档中的 OkHttp3实现Cookies管理及持久化,里面已经够详细了。

问题4:NetworkOnMainThreadException

下面这段代码似乎没错,不是说OkHttp会在线程池中访问网络吗?怎么会报这种错误??
@Override
protected void onResume() {
    super.onResume();
    Request request = new Request.Builder()
            .url("https://github.com")
            .build();
    okHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
        }

        @Override
        public void onResponse(Call call, final Response response) throws IOException {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String string = response.body().string(); //注意
                        helloTxt.setText(string);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    });
}
解决:在标注的那一行 response.body().string(),是在主线程中运行的,从响应中获取响应体属于网络操作,所以报错。解决方法是将这一行移到 runOnUiThread 方法前面。

Android 平台的檔案讀寫方式

處理檔案是程式開發過程中常會碰到的問題,在 Android 平台上讀寫檔案的也是利用 Java 的 File、InputStream 以及 OutputStream 物件來達成。不過 Android 系統對 App 的使用空間與檔案操作有一套自己的管理方式,透過系統提供的 Context 與 Environment 物件可以讓開發人員快速的進行檔案的各種操作。


A. 使用的物件以及方法

  1. Context
    • abstract boolean deleteFile(String name)
    • abstract String[] fileList()
    • abstract File getCacheDir()
    • abstract File getDir(String name, int mode)
    • abstract File getExternalCacheDir()
    • abstract File getExternalFilesDir(String type)
    • abstract File getFileStreamPath(String name)
    • abstract File getFilesDir()
    • abstract FileInputStream openFileInput(String name)
    • abstract FileOutputStream openFileOutput(String name, int mode)
  2. Environment
    • static File getDataDirectory()
    • static File getDownloadCacheDirectory()
    • static File getExternalStorageDirectory()
    • static File getExternalStoragePublicDirectory(String type)
    • static String getExternalStorageState()
    • static File getRootDirectory()
    • static boolean isExternalStorageEmulated()
    • static boolean isExternalStorageRemovable()

    B. 原理說明

    1. 在 Android 設備上的儲存體 (storage) 可分為內部 (internal) 以及外部(external) 兩種,內部儲存體指的是內建的 Flash,外部儲存體指的是外接的 SD 卡。有些設備即使沒有外接的儲存設備,Android 系統也會將儲存體分為內部以及外部兩個區域。因此,內部儲存體一定存在,外部儲存體則不一定,如果沒有外接儲存設備就不會有外部儲存體。

    2. 在預設的情況下 App 會將新建立檔案存在內部儲存體,存在內部儲存體的檔案預設只能被該 App 存取。當 App 被移除時,儲存在該空間的檔案也會一併被刪除。因此,內部儲存體適合用來擺放專屬於該 App 的檔案,當 App 被移除時這些檔案也沒有存在的必要。

    3. 除了內部儲存體外,App 也可以將檔案存放在外部儲存體,放在外部儲存體的檔案可以被其他的 App 讀取。當 App 被移除時,存放在外部儲存體的檔案並不會被移除,唯一的例外是存放在 getExternalFilesDir() 目錄底下的檔案會被移除 (該目錄底下的檔案算是 App 的私有檔案,雖然是放在外部儲存體,不過 App 被移除時系統也會將檔案刪除)。

    4. 在安裝 App 時預設會裝在內部儲存體,也可以在 AndroidManifest.xml 中設定 android:installLocation 屬性,將 App 安裝在外部儲存體 (除非 App 的大小超過內部儲存體的空間大小,否則很少這樣做)。
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          android:installLocation=["auto" | "internalOnly" | "preferExternal"]
          ...
      </manifest>
    5. 在預設的情況下 App 具有讀/寫內部儲存體的權限,因此,可以讀取 (read) 或寫入 (write) 內部儲存體裡面的檔案,並不需要在 AndroidManifest.xml 中宣告額外的權限。

    6. 在預設的情況下,App 具有讀取 (沒有寫入) 外部儲存體的權限,不過這個權限在未來的 Android 版本可能會做調整,因此,若 App 有讀取外部儲存體的需求,最好還是在 AndroidManifest.xml 檔案中宣告 READ_EXTERNAL_STORAGE 的權限會比較保險,如:
      <manifest ...>
          <uses-permission 
           android:name="android.permission.READ_EXTERNAL_STORAGE" />
          ...
      </manifest>
      
    7. 如果要將檔案存放在外部儲存體,必須取得寫入外部儲存體的權限才行,因此要在 AndroidManifest.xml 中宣告 WRITE_EXTERNAL_STORAGE 權限:
      <manifest ...>
          <uses-permission 
           android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
          ...
      </manifest>
      如果 App 具有寫入外部儲存體的權限,隱含的意義就是該 App 也同時取得了讀取外部儲存體的權限 (能夠寫入就表示一定能夠讀取)。

    8. 在 Android 平台上讀寫檔案的方式是透過 java.io.File 物件來達成,至於檔案的擺放位置或建立檔案的方式,可透過 Context 物件裡面的以下方法來達成:
      • abstract File getFilesDir()
        取得 App 內部儲存體存放檔案的目錄 (絕對路徑)
        預設路徑為 /data/data/[package.name]/files/
      • abstract File getCacheDir()
        取得 App 內部儲存體存放暫存檔案的目錄 (絕對路徑)
        預設路徑為 /data/data/[package.name]/cache/
      • abstract File getExternalFilesDir(String type)
        取得 App 外部儲存體存放檔案的目錄 (絕對路徑)
      • abstract File getExternalCacheDir()
        取得 App 外部儲存體存放暫存檔案的目錄 (絕對路徑)
      • abstract File getDir(String name, int mode)
        取得 App 可以擺放檔案的目錄,若該目錄不存在則建立一個新的
        ex: getDir("music", 0) -> /data/data/[package.name]/app_music
      • abstract boolean deleteFile(String name)
        刪除 getFilesDir() 目錄底下名稱為 name 的檔案
      • abstract String[] fileList()
        回傳 getFilesDir() 目錄底下的檔案及目錄名稱
      • abstract FileInputStream openFileInput(String name)
        開啟 getFilesDir() 目錄下檔名為 name 的檔案來進行讀取
      • abstract FileOutputStream openFileOutput(String name, int mode)
        在 getFilesDir() 目錄底下開啟或建立檔名為 name 的檔案來進行寫入
      • abstract File getFileStreamPath(String name)
        取得 openFileOutput() 所建立之名稱為 name 的檔案的絕對路徑
    1. Environment 物件提供 Android 系統環境的相關資訊,包含外部儲存體的狀態,以及相關檔案的擺放位置,如:
      • static File getDataDirectory()
        取得系統的資料擺放目錄,預設位置為 /data
      • static File getDownloadCacheDirectory()
        取得系統檔案下載或暫存檔案的擺放目錄,預設位置為 /cache
      • static File getExternalStorageDirectory()
        取得外部儲存體的根目錄,預設位置為 /mnt/sdcard
      • static File getExternalStoragePublicDirectory(String type)
        取得外部儲存體存放公開檔案的目錄
      • static String getExternalStorageState()
        取得外部儲存體的狀態資訊
      • static File getRootDirectory()
        取得檔案系統的根目錄,預設位置為 /system
      • static boolean isExternalStorageEmulated()
        判斷外部儲存體是否使用內部儲存體模擬產生
        true: 外部儲存體不存在,而是使用內部儲存體模擬產生
        false: 外部儲存體存在,並非使用內部儲存體模擬
      • static boolean isExternalStorageRemovable()
        判斷外部儲存體是否可以移除,回傳值的意義如下:
        true: 外部儲存體屬於外接式的,且可以移除
        false: 外部儲存體內建在系統中,無法被移除
    2. 當 Android 系統發現空間不足時,會將存放在暫存目錄 getCacheDir() 裡面的檔案刪除。因此,App 在執行時不能假設存放在該目錄裡面的檔案一定存在,也不能假設該目錄底下的檔案一定會被系統刪除,最好是在檔案不用時 App 自己將它刪除,以免占用內部儲存體的空間。

    3. 由於外部儲存體不一定存在,所以在使用前必須先檢查它的狀態,以避免在讀寫時發生錯誤。透過 Environment 物件的 getExternalStorageState() 方法可以查詢目前外部儲存體的狀態,其中狀態可以是以下這幾種:
      • MEDIA_BAD_REMOVAL: 外部儲存體在正常卸載之前就被拔除
      • MEDIA_CHECKING: 外部儲存體存在且正在進行磁碟檢查
      • MEDIA_MOUNTED: 外部儲存體存在且可以進行讀取與寫入
      • MEDIA_MOUNTED_READ_ONLY: 外部儲存體存在但只能進行讀取
      • MEDIA_NOFS: 外部儲存體存在,但內容是空的或是 Android 不支援該檔案系統
      • MEDIA_REMOVED: 外部儲存體不存在
      • MEDIA_SHARED: 外部儲存體存在但未被掛載,且為 USB 的裝置
      • MEDIA_UNMOUNTABLE: 外部儲存體存在但不能被掛載
      • MEDIA_UNMOUNTED: 外部儲存體存在但未被掛載
    1. 外部儲存體的另一個涵義指的是所有 App 的共用空間,對 App 來說存放在外部儲存體的檔案可以分為公開檔案 (public files) 與私有檔案 (private files) 兩種。擺放在 Environment.getExternalStoragePublicDirectory() 目錄底下的為公開檔案,擺放在 Context.getExternalFilesDir() 目錄底下的為私有檔案。

    2. 公開檔案就像是照片或是音樂,由目前 App 產生可以提供其他 App 使用的檔案。私有檔案就像是 App 執行時產生的暫存檔,對其他 App 來說並沒有使用上的價值。擺放在外部儲存體的檔案都可以被其他 App 存取,不過當 App 被移除時,只有私有檔案會被移除,公開檔案並不會被移除。

    3. 由於公開檔案可以提供其它 App 使用,所以在放置這些檔案時 Android 系統提供了一些基本的分類,讓 App 可以依檔案屬性將檔案放置在不同目錄裡面,方便其它 App 可以使用。因此,getExternalStoragePublicDirectory(String type) 可以接受一個 type 參數,該參數表示目錄中儲存的檔案型態,例如:getExternalStoragePublicDirectory(DIRECTORY_PICTURES) 會回傳用來擺放圖片檔的目錄,如果 App 產生的圖片要提供給其它 App 使用,就可以擺放在這個目錄。目前 Android 定義的目錄型態包含以下這幾種:
      • DIRECTORY_ALARMS: 鬧鐘的音效檔
      • DIRECTORY_DCIM: 相機的圖片與影片檔
      • DIRECTORY_DOWNLOADS: 使用者下載的檔案
      • DIRECTORY_MOVIES: 電影檔
      • DIRECTORY_MUSIC: 音樂檔
      • DIRECTORY_NOTIFICATIONS: 通知音效檔
      • DIRECTORY_PICTURES: 一般的圖片檔
      • DIRECTORY_PODCASTS: 訂閱的廣播檔
      • DIRECTORY_RINGTONES: 鈴聲檔
      type 參數如果為 null 時可取得擺放公開檔案的根目錄,如果 App 要擺放的檔案型態不屬於上述那幾類,也可以直接將檔案擺放在根目錄。
      1. 將資料寫入儲存體時如果造成空間不足就發產生 IOException,使用 File 物件的 getTotalSpace() 與 getFreeSpace() 可以取得儲存體的總容量與剩餘空間資訊 (單位是 bytes)。如果可以事先知道要寫入的檔案大小,就可以在寫入前先判斷剩餘空間是否足夠,以避免寫入過程發生錯誤。

      C. 使用方式

      1. 將資料寫入內部儲存體的檔案中

      (1) 將檔案存放在 getFilesDir() 目錄
      //**** 方法一 ****//
      //取得內部儲存體擺放檔案的目錄
      //預設擺放路徑為 /data/data/[package.name]/files/
      File dir = context.getFilesDir();
      
      //在該目錄底下開啟或建立檔名為 "test.txt" 的檔案
      File outFile = new File(dir, "test.txt");
      
      //將資料寫入檔案中,若 package name 為 com.myapp
      //就會產生 /data/data/com.myapp/files/test.txt 檔案
      writeToFile(outFile, "Hello! 大家好");
      
      ...
      
      //writeToFile 方法如下
      private void writeToFile(File fout, String data) {
          FileOutputStream osw = null;
          try {
              osw = new FileOutputStream(fout);
              osw.write(data.getBytes());
              osw.flush();
          } catch (Exception e) {
              ;
          } finally {
              try {
                  osw.close();
              } catch (Exception e) {
                  ;
              }
          }
      }
      
      
      //**** 方法二 ****//
      FileOutputStream out = null;
      try {
          //在 getFilesDir() 目錄底下建立 test.txt 檔案用來進行寫入
          out = openFileOutput("test.txt", Context.MODE_PRIVATE);
      
          //將資料寫入檔案中
          out.write("Hello! 大家好\n".getBytes());
          out.flush();
      } catch (Exception e) {
          ;
      } finally {
          try {
              out.close();
          } catch (Exception e) {
              ;
          }
      }
      
      (2) 將檔案存放在 getCacheDir() 目錄
      //取得內部儲存體擺放暫存檔案的目錄
      //預設擺放路徑為 /data/data/[package.name]/cache/
      File dir = context.getCacheDir();
      
      //在該目錄底下開啟或建立檔名為 "test.txt" 的檔案
      File outFile1 = new File(dir, "test.txt");
      
      //也可以使用 File.createTempFile() 來建立暫存檔案
      File outFile2 = File.createTempFile("test", ".txt", dir);
      
      //將資料寫入檔案中,若 package name 為 com.myapp
      //就會產生 /data/data/com.myapp/cache/test.txt 檔案
      writeToFile(outFile1, "Hello! 大家好");
      
      //產生 /data/data/com.myapp/cache/test-[亂數].txt 檔案
      writeToFile(outFile2, "Hello! 大家好");

      2. 讀取內部儲存體中的檔案內容

      //** 方法一 **//
      //取得內部儲存體擺放檔案的目錄
      //預設擺放目錄為 /data/data/[package.name]/
      File dir = context.getFilesDir();
      
      //開啟或建立該目錄底下檔名為 "test.txt" 的檔案
      File inFile = new File(dir, "test.txt");
      
      //讀取 /data/data/com.myapp/test.txt 檔案內容
      String data = readFromFile(inFile);
      
      ...
      
      //readFromFile 方法如下
      private String readFromFile(File fin) {
          StringBuilder data = new StringBuilder();
          BufferedReader reader = null;
          try {
              reader = new BufferedReader(new InputStreamReader(
                       new FileInputStream(fin), "utf-8"));
              String line;
              while ((line = reader.readLine()) != null) {
                  data.append(line);
              }
          } catch (Exception e) {
              ;
          } finally {
              try {
                  reader.close();
              } catch (Exception e) {
                  ;
              }
          }
          return data.toString();
      }
      
      
      
      //** 方法二 **//
      FileInputStream in = null;
      StringBuffer data = new StringBuffer();
      try {
          //開啟 getFilesDir() 目錄底下名稱為 test.txt 檔案
          in = openFileInput("test.txt");
      
          //讀取該檔案的內容
          BufferedReader reader = new BufferedReader(
                         new InputStreamReader(in, "utf-8"));
          String line;
          while ((line = reader.readLine()) != null) {
             data.append(line);
          }
      } catch (Exception e) {
          ;
      } finally {
          try {
              in.close();
          } catch (Exception e) {
              ;
          }
      }
      
      

      3. 將資料寫入外部儲存體的檔案中

      (1) 檢查外部儲存體的狀態是否可以讀寫
      //檢查外部儲存體是否可以進行寫入
      public boolean isExtStorageWritable() {
          String state = Environment.getExternalStorageState();
          if (Environment.MEDIA_MOUNTED.equals(state)) {
              return true;
          }
          return false;
      }
      
      //檢查外部儲存體是否可以進行讀取
      public boolean isExtStorageReadable() {
          String state = Environment.getExternalStorageState();
          if (Environment.MEDIA_MOUNTED.equals(state) ||
              Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
              return true;
          }
          return false;
      }
      
      (2) 將檔案存放在外部儲存體 (私有檔案)
      //將檔案存放在 getExternalFilesDir() 目錄
      if (isExtStorageWritable()){
          File dir = context.getExternalFilesDir(null);
          File outFile = new File(dir, "test.txt");
          writeToFile(outFile, "Hello! 大家好");
      }
      
      //將檔案存放在 getExternalCacheDir() 目錄
      if (isExtStorageWritable()){
          File dir = context.getExternalCacheDir();
          File outFile = new File(dir, "test.txt");
          writeToFile(outFile, "Hello! 大家好");
      }
      (3) 將檔案存放在外部儲存體 (公開檔案)
      //取得存放公開圖片檔的目錄,並在該目錄下建立 subDir 子目錄
      public File getExtPubPicDir(String subDir) {
          File file = new File(Environment.getExternalStoragePublicDirectory(
                  Environment.DIRECTORY_PICTURES), subDir);
          //若目錄不存在則建立目錄
          if (!file.mkdirs()) {
              Log.e(LOG_TAG, "無法建立目錄");
          }
          return file;
      }
      
      ...
      
      //取得外部儲存體存放圖片公開檔案目錄底下的 flowers 子目錄
      File path = getExtPubPicDir("flowers");
      
      //在該目錄下建立檔名為 flower.jpg 的檔案
      File file = new File(path, "flower.jpg");
      
      //將圖片內容由 App 拷貝到該目錄下
      InputStream is = getResources().openRawResource(R.drawable.flower);
      OutputStream os = new FileOutputStream(file);
      
      byte[] buffer = new byte[1024];
      while (true) {
          int bytesRead = in.read(buffer);
          if (bytesRead == -1) break;
          os.write(buffer, 0, bytesRead);
      }
      
      is.close();
      os.close();

      4. 刪除檔案

      當 App 被移除時,Android 系統會刪除所有由該 App 產生存放在內部儲存體的檔案,以及存放在外部儲存體的私有檔案 (Context.getExternalFilesDir() 目錄底下的檔案),不過最好還是在檔案不用時就將它刪除,以免佔用不必要的空間。
      //刪除暫存目錄中 test.txt 檔案
      File f = new File(context.getCacheDir(),"test.txt");
      f.delete();
      
      //刪除 getFilesDir() 目錄底下 test.txt 檔案
      context.deleteFile("test.txt");