來源: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
facebook廣告
2016年10月22日 星期六
2016年10月5日 星期三
将String转换成InputStream
String str = "";//add your string content
InputStream inputStream = new ByteArrayInputStream(str.getBytes());
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介面及如何啟動執行緒:
將程式編譯並執行時,您可以看到一個視窗,按下上面的按鈕,您會看到兩個圓在「同時」繪製,雖說是同時,其實也只是錯覺而已,其實是CPU往來兩個流程之間不斷的進行繪製圓的動作而已。
Thread類別也實作了Runnable介面,您也可以繼承Thread類別並重新定義它的run()方法,好處是可以使用Thread上的一些繼承下來的方法,例如yield(),然而繼承了Thread就表示您不能讓您的類別再繼承其它的類別。
引用:http://openhome.cc/Gossip/JavaGossip-V2/RunnableInterface.htm
一個執行緒是進程中的一個執行流程,一個進程中可以同時包括多個執行緒,也就是說一個程式中同時可能進行多個不同的子流程,這使得一個程式可以像是同時間 處理多個事務,例如一方面接受網路上的資料,另一方面同時計算資料並顯示結果,一個多執行緒程式可以同時間處理多個子流程。
在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脑图
出现背景
网络访问的高效性要求,可以说是为高效而生
解决思路
- 提供了对 HTTP/2 和 SPDY 的支持,这使得对同一个主机发出的所有请求都可以共享相同的套接字连接
- 如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率
- 提供了对 GZIP 的默认支持来降低传输内容的大小
- 提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求
- 当网络出现问题时,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)异步调用
解决:参见 (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. 使用的物件以及方法
- 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)
- 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. 原理說明
- 在 Android 設備上的儲存體 (storage) 可分為內部 (internal) 以及外部(external) 兩種,內部儲存體指的是內建的 Flash,外部儲存體指的是外接的 SD 卡。有些設備即使沒有外接的儲存設備,Android 系統也會將儲存體分為內部以及外部兩個區域。因此,內部儲存體一定存在,外部儲存體則不一定,如果沒有外接儲存設備就不會有外部儲存體。
- 在預設的情況下 App 會將新建立檔案存在內部儲存體,存在內部儲存體的檔案預設只能被該 App 存取。當 App 被移除時,儲存在該空間的檔案也會一併被刪除。因此,內部儲存體適合用來擺放專屬於該 App 的檔案,當 App 被移除時這些檔案也沒有存在的必要。
- 除了內部儲存體外,App 也可以將檔案存放在外部儲存體,放在外部儲存體的檔案可以被其他的 App 讀取。當 App 被移除時,存放在外部儲存體的檔案並不會被移除,唯一的例外是存放在 getExternalFilesDir() 目錄底下的檔案會被移除 (該目錄底下的檔案算是 App 的私有檔案,雖然是放在外部儲存體,不過 App 被移除時系統也會將檔案刪除)。
- 在安裝 App 時預設會裝在內部儲存體,也可以在 AndroidManifest.xml 中設定 android:installLocation 屬性,將 App 安裝在外部儲存體 (除非 App 的大小超過內部儲存體的空間大小,否則很少這樣做)。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation=["auto" | "internalOnly" | "preferExternal"] ... </manifest>
- 在預設的情況下 App 具有讀/寫內部儲存體的權限,因此,可以讀取 (read) 或寫入 (write) 內部儲存體裡面的檔案,並不需要在 AndroidManifest.xml 中宣告額外的權限。
- 在預設的情況下,App 具有讀取 (沒有寫入) 外部儲存體的權限,不過這個權限在未來的 Android 版本可能會做調整,因此,若 App 有讀取外部儲存體的需求,最好還是在 AndroidManifest.xml 檔案中宣告 READ_EXTERNAL_STORAGE 的權限會比較保險,如:
<manifest ...> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ... </manifest>
- 如果要將檔案存放在外部儲存體,必須取得寫入外部儲存體的權限才行,因此要在 AndroidManifest.xml 中宣告 WRITE_EXTERNAL_STORAGE 權限:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
如果 App 具有寫入外部儲存體的權限,隱含的意義就是該 App 也同時取得了讀取外部儲存體的權限 (能夠寫入就表示一定能夠讀取)。 - 在 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 的檔案的絕對路徑
- 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: 外部儲存體內建在系統中,無法被移除 - 當 Android 系統發現空間不足時,會將存放在暫存目錄 getCacheDir() 裡面的檔案刪除。因此,App 在執行時不能假設存放在該目錄裡面的檔案一定存在,也不能假設該目錄底下的檔案一定會被系統刪除,最好是在檔案不用時 App 自己將它刪除,以免占用內部儲存體的空間。
- 由於外部儲存體不一定存在,所以在使用前必須先檢查它的狀態,以避免在讀寫時發生錯誤。透過 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: 外部儲存體存在但未被掛載
- 外部儲存體的另一個涵義指的是所有 App 的共用空間,對 App 來說存放在外部儲存體的檔案可以分為公開檔案 (public files) 與私有檔案 (private files) 兩種。擺放在 Environment.getExternalStoragePublicDirectory() 目錄底下的為公開檔案,擺放在 Context.getExternalFilesDir() 目錄底下的為私有檔案。
- 公開檔案就像是照片或是音樂,由目前 App 產生可以提供其他 App 使用的檔案。私有檔案就像是 App 執行時產生的暫存檔,對其他 App 來說並沒有使用上的價值。擺放在外部儲存體的檔案都可以被其他 App 存取,不過當 App 被移除時,只有私有檔案會被移除,公開檔案並不會被移除。
- 由於公開檔案可以提供其它 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 要擺放的檔案型態不屬於上述那幾類,也可以直接將檔案擺放在根目錄。
- 將資料寫入儲存體時如果造成空間不足就發產生 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");
訂閱:
文章 (Atom)