JavaSE學習過程中問題總結 —— 集合框架、IO流、多執行緒等
回顧完基本概念和一些常用類,進入重點:
(內容很多但沒有分文章寫,主要是自己的知識鞏固和做下筆記,不夠負責任,算不上知識分享)
一、集合框架
1、集合引入
(1)物件陣列的概念:將多個類物件存放為陣列
特點:長度固定,不適應變化的需求。
(2)面向物件強調的是物件,需要容器來進行儲存和操作
之前提過StringBuffer雖然是一種容器,但只存放字串
如果沒有集合,我們只能選擇物件陣列,但並不能滿足需求
所以Java提供了集合,方便對多個物件進行操作
(3)集合與陣列的區別
陣列可以儲存物件,也可以儲存基本型別,並且存放的都是同一型別元素
集合是可變的,但只能儲存物件,可以儲存不同型別元素(但其實一般還是同類型)
(4)集合框架的繼承體系
存放多個元素時,我們可能有不同的需求
例如是否允許有重複元素,是否需要有序存放,或是是否需要進行排序等
針對多樣的需求,Java也為我們提供了不同的集合類
這些類資料結構不同,功能不同;但也存在共性,所以他們存在一個繼承體系
基類:Collection
兩個分支:List與Set
List分支下三個子類:ArrayList、Vector、LinkedList
Set分支下兩個子類:HashSet、TreeSet
* 繼承體系: * Collection * / \ * List Set * / | \ | \ * ArrayList Vector LinkedList HashSet TreeSet
所以我們從基類開始學起。
2、Collection集合
(1)集合框架體系的基類
它是一個介面,表示一組物件
一些允許重複元素出現,另一些則不允許;一些有序,另一些無序
Jdk不提供該介面的任何具體實現,提供了子介面:List和Set
(2)基本功能
新增和刪除:
boolean add(E e);新增功能,而返回布林型別,表示是否新增成功。例如當集合發生了改變,則返回true;再比如集合中出現了原來有的元素,而子實現類集合不允許重複,則返回false。
addAll(Collection c);將指定集合中的元素都新增到該集合,也返回布林型別
clear();清空集合,很暴力的方法
remove(obj);刪除指定元素的單個例項
removeAll(c);刪除集合中也包含在指定集合的所有元素,可以說是刪除交集;只要有一個元素被移除了,就會返回true
一些簡單的判斷和獲取一些值的方法:
contains(obj);判斷是否包含指定元素,也有對應的all方法
equals、isEmpty、hashCode、size;都是字面就能理解的方法,其實都是API內容,寫一遍加深印象
這裡要注意的是長度這個屬性,在各個容器中都有區別:
陣列是直接以點號呼叫length屬性來獲取長度,沒有length()方法
字串是length()方法獲取長度,所以可能經常習慣性把集合也記為length
集合中是用size()方法返回長度
其他方法:
iterator();返回迭代器
retainAll();保留兩個集合都有的元素,可以說是取交集;但是要注意這裡返回的布林型別值,並不是根據有無交集判斷true還是false,而是判斷集合是否發生了改變,首先要明白,取了交集後,結果是體現在呼叫該方法的集合中,也就是該集合變成了最後的交集,只要他發生了改變,就返回true,所以即使沒有交集,他成為了空集合,也是true的結果
toArray();集合轉為陣列
(3)集合的遍歷方式
方式一:通過toArray方法,轉換為陣列遍歷
方式二:迭代器遍歷,iterator方法返回迭代器物件
Iterator是一個介面,有兩個方法組合使用可以實現遍歷
next();獲取下一個元素;如果不進行判斷一直獲取,到集合末尾會產生異常NoSuchElementException
hasNext();判斷是否還有下一個元素,有了這個作為判斷,可以防止異常發生
迭代器遍歷有兩種寫法:
假設我有一個集合c存放了一些學生物件,那麼遍歷基本格式如下
Iterator i = c.iterator();// 這個方法返回的是Iterator子類物件
while (i.hasNext()) {//判斷是否有下一個元素,有就一直迴圈遍歷
// 此時沒有加入泛型,返回的都是object物件,最好進行向下轉型
Student s = (Student) i.next();
System.out.println(s.getName() + "-------" + s.getAge());//拿到每個物件輸出屬性值
}
這種while迴圈的結構較清晰,但為了讓迭代器物件用完就能被回收,經常還有一種for迴圈寫法
// for改寫
for (Iterator ii = c.iterator(); ii.hasNext();) {
Student s = (Student) ii.next();
System.out.println(s.getName() + "---------" + s.getAge());
}//for迴圈結束,迭代器物件立馬變為垃圾,節省空間
方式三:JDK5新特性,增強for迴圈遍歷,簡單方便
(4)小結
集合的一般使用步驟:
建立集合物件、建立元素物件、將元素新增到集合中、遍歷集合進行使用。
迭代器原理:
集合的種類很多,資料結構不同,功能也不同,所以儲存和遍歷的方式肯定也有差異;所以迭代器被定義為一個抽象介面,只提供共性的功能,而不提供具體實現;而我們為什麼能使用他,他的具體實現又是怎麼來的,就要去結合原始碼理解,其實是在集合各種具體子類中以內部類的方式實現的,這樣就滿足了不同的需求。
3、List集合
(1)Collection下的一個子介面
所以Collection理解之後,List就幾乎等於理解了,但既然他是一個分支,就必然有特殊之處。
(2)特點
List是有序的集合,但集合中的有序,並不是平時所說的排序
而是指新增元素時什麼順序,輸出是就是什麼順序,可以通過索引進行精準操作
List允許重複元素的出現,他的三個子類也都具有這些特點
(3)擴充套件功能
正因為他有序的特性,就有一些對應的根據索引操作的方法
add(int index,Object element);可以在指定位置新增元素;可能產生IndexOutOfBounds異常,但可以在結尾新增
get(index);根據索引獲取元素;反之有根據元素獲取索引的indexOf方法
set(index,element);將指定位置元素替換為指定元素
subList();結合substring理解,擷取集合
特殊的迭代器ListIterator
繼承於普通迭代器,用法也基本相同,但有特殊的方法
對應於hasNext和next,提供了hasPrevious和previous方法
字面上很好理解,就是向前遍歷;雞肋的是,這兩個方法必須在用完了向後遍歷後才能使用,類似於一個指標,剛開始都是指在集合前端,只有遍歷到最後,指標也指在結尾時才能向前遍歷,所以似乎並沒有什麼用處。
4、簡單資料結構
(1)理解集合
說完了List,就要開始提他的具體子類了,而他們大多是根據底層資料結構來劃分的,所以為了更好的理解他們各自的特性和功能的差異,不得不先簡單回顧下一些常見資料結構。
(2)棧與佇列,陣列
棧,是一種先進後出的結構(例如彈夾)
佇列,先進先出(就像排隊)
陣列,儲存同一型別元素的固定容器
他們有共同的特點,因為有序,所以能夠根據索引進行精準的定位,查詢快
但如果想要新增或刪除元素,需要進行大量的移位操作,還要分配空間,增刪效率較慢
(3)連結串列
由資料域和指標域組成,用一條鏈把多個結點連在一起
查詢時,每次都要從頭遍歷直到找到元素
增刪時,只需要操作指標域進行指向的修改
所以連結串列的特點是查詢慢,增刪快
理解了這些基本內容,就可以來說List三個具體子類了。
5、ArrayList集合
(1)List分支下應用最多的集合
繼承於List,元素有序,允許出現重複元素
(2)特點
底層資料結構為陣列,查詢快,增刪慢
執行緒不安全,效率高
(3)用法
Collection理解之後,看完了List,ArrayList基本就相當於掌握了,用法基本一致
6、Vector集合
(1)可以說是ArrayList的前身
從1.0出現的集合類,其他類還是1.2才出現。
(2)特點
執行緒安全,效率較低
(3)方法
addElement(obj);新增元素
elementAt(index);獲取元素
elements();早期的迭代器,返回Enumeration物件
(4)jdk升級原因
從這個類可以看出,jdk進行升級原因一般有,安全問題,效率問題,還有就是想要簡化書寫;Vector類中的方法名過於長,不方便使用。
7、LinkedList集合
(1)特點
底層資料結構為連結串列,查詢慢,增刪快
執行緒不安全,效率高
(2)特有功能
addFirst(obj);將元素新增到集合開頭,有對應的last方法新增到末尾
getFirst();獲取第一個元素,也有對應的last方法獲取最後一個元素
removeFirst();刪除第一個元素,也有removeLast方法
8、List分支下集合的一些小問題
(1)有序、允許重複
雖然有專門的集合不允許重複元素出現,但例如就是想要去除ArrayList中重複元素怎麼做
思路一:建立一個新的集合,遍歷舊集合往新集合裡新增,每次進行判斷,新集合中是否包含要新增的元素
for (Iterator i = list.iterator(); i.hasNext();) {//遍歷舊集合
String s = (String) i.next();
if (!newList.contains(s)) {//判斷新集合是否包含每個集合
nl.add(s);//有則新增
}
}
思路二:在原集合上操作,根據選擇排序的思想,每個元素與後面元素比較,相同則移除後面的元素
for (int i = 0; i < list.size() - 1; i++) {//外層迴圈控制拿到每個元素
for (int j = i + 1; j < list.size(); j++) {//內層迴圈與外層元素後面所有元素比較
if (list.get(i).equals(list.get(j))) {//判斷是否相同
list.remove(j);//相同則移除後面的元素
j--;// 一定要注意,移除了一個元素之後,後面的元素補上來,所以內層要重新從對應位置再比較一次
}
}
}
要注意的是思路一中,contains方法原始碼中,判斷是否包含指定元素時,呼叫的是equals方法,例子中是以字串元素物件舉例,重寫過equals方法;如果元素是自定義類,一定要記得重寫equals方法,否則Object類中,equals就相當於==,即使物件屬性值都相同,地址值不同的話也不能被檢測出來。
(2)根據LinkedList特點,模擬棧資料結構
思路:LinkedList底層資料結構為連結串列,有addFirst方法,可以模擬壓棧
// 思路:自己寫一個集合類,基本方法有add和get,具體實現直接用Link模擬
class MyList {
// 建立一個LinkedList用於模擬方法
private LinkedList list;
public MyList() {
// 構造時才建立物件
list = new LinkedList();
}
public void add(Object obj) {
// 直接用LinkedList特有的方法模擬壓棧
list.addFirst(obj);
}
public Object get() {
// 如果用getFirst,永遠獲取第一個
// 而用remove,返回被移除物件,也模擬了彈棧過程
return list.removeFirst();
}
// 最好加一個判斷為空,輸出時更嚴謹
public boolean isEmpty() {
return list.isEmpty();
}
}
測試
public static void main(String[] args) {
MyList l = new MyList();
l.add("world");
l.add("hello");
while (!l.isEmpty()) {
System.out.println(l.get());
}
注意事項:
首先是問題的理解,剛開始我會理解為,用LinkedList集合模擬出棧結構的出入棧過程
而這個題目的點在於自定義集合,所以意思其實是讓你寫一個類出來,只是可以用LinkedList作為底層
壓棧用addFirst方法比較容易想到,但彈棧如果用getFirst,就只能一直獲取第一個元素
所以採用了特有的removeFirst,這個方法正好返回被移除的元素,並且也模擬了彈棧過程
一個小細節就是重寫一下isEmpty,這樣輸出時讓while迴圈有一個出口
9、泛型
(1)引入
之前說集合可以存放不同型別的元素,但其實並不會這樣用,但如果不小心添加了不同元素,編譯期不會報錯,只有在執行時,到了向下轉型的階段,才會被發現有問題,那麼怎麼解決?集合中提供了泛型的概念,可以對型別進行約束。可以有效避免ClassCastException。
(2)優點
將執行期才會被發現的問題提前到了編譯時期
避免了強制型別轉換,因為從一開始就限定了元素型別
優化程式設計,減少黃色警告線,使程式設計更嚴謹
(3)其他應用
除了集合中用泛型,還有泛型類、方法、介面,可以自己隨便寫幾個理解一下,注意一下格式即可
//自己寫一個泛型類
class MyGenericList<E> {
private ArrayList<E> list;
public MyGenericList() {
list = new ArrayList<E>();
}
//泛型方法
public void add(E e) {
list.add(e);
}
public E get(int index) {
return list.get(index);
}
}
//非泛型類中使用泛型方法
class Tool {
//要在方法中定義出泛型
public <E> void show(E e) {
System.out.println(e);
}
}
//泛型定義在介面上
interface Inter<E> {
public abstract void show(E e);
}
//實現類,如果直接知道用什麼型別實現,實現類名後不用跟泛型
class InterDemo<E> implements Inter<E> {
@Override
public void show(E e) {
// TODO Auto-generated method stub
System.out.println(e);
}
}
(4)高階萬用字元
在泛型宣告時,要求前後必須要一致,即使後面泛型與前面有繼承關係,也不能這樣宣告
所以Java提供了一個萬用字元“?”,表示任意型別,預設都繼承與Object
? extends E,表示向下限定,可以宣告E繼承體系之下的泛型
? super E,表示向上限定,可以宣告E以及E父類的泛型
10、集合與陣列
(1)集合轉換為陣列
之前提過集合有一個方法toArray可以轉換為陣列
(2)陣列轉換為集合
Arrays工具類中,提供了一個方法可以把陣列轉換為集合
public static <T> List<T> asList(T... a);
(3)注意事項
陣列轉換為集合之後,本質上還是一個數組,保留了陣列長度固定的特性
這個集合只能修改,不能增刪
11、一些練習
(1)獲取十個1-20隨機數,要求不能重複,但使用ArrayList存放
//獲取隨機數方法
public static void getRandom () {
//建立一個集合存放隨機數
ArrayList<Integer> list = new ArrayList<Integer>();
//產生與新增
for (int i = 0; i < 10;i++) {
int num = (int) (Math.random()*20 + 1);
if (!list.contains(num)) {
list.add(num);
} else {
i--;//注意點:如果包含,這次迴圈不算數
}
}
//遍歷輸出
for (int i : list) {
System.out.println(i);
}
}
(2)鍵盤錄入一些值,取最大
public static int getMax() {
int max = 0;
Scanner sc = new Scanner(System.in);
ArrayList<Integer> list = new ArrayList<Integer>();
System.out.println("輸入多個數字,以0結束");
while (true) {
int num = sc.nextInt();
if (num == 0) {//以0為結束,做排序後跳出迴圈
//轉換成陣列,讓陣列中的方法幫我們做排序
Integer[] i = new Integer[list.size()];
list.toArray(i);
Arrays.sort(i);
max = i[i.length-1];
break;
} else {//輸入0之前一直新增
list.add(num);
}
}
return max;
}
12、Set集合
(1)Collection的另一個分支
元素無序、唯一的集合
(2)方法差異
add方法會判斷是否包含該元素,保證元素唯一性
沒有get方法,因為無序不能使用索引定位
遍歷只能使用迭代器或增強for,不能使用普通for,原因也是因為無序
13、HashSet
(1)繼承於Set集合
不保證set的迭代順序,特別是不保證該順序恆久不變
元素唯一,底層資料結構是雜湊表,依賴於雜湊值儲存
(2)保證元素唯一
新增功能依賴於兩個方法hashCode與equals
字串中重寫了這些方法,測試時可以直接新增保證唯一
如果要存放自定義物件,自定義類中一定要記得重寫兩個方法
否則預設呼叫Object類中equals,比較的是地址值,而內容是否一致沒有比較
(3)一個子類集合LinkedHashSet
由連結串列和雜湊表結構組成,綜合了他們的特點
由連結串列保證了元素有序
由雜湊表保證了元素唯一
14、TreeSet集合
(1)Set分支下的另一個集合
底層是紅黑樹結構(一種自平衡二叉樹)
元素唯一,能夠對元素按照規則進行排序(是排序不是有序)
(2)排序方式
方式一:自然排序
呼叫無參構造預設使用自然排序
是通過類實現comparable介面,重寫compareTo方法,在方法中規定排序規則
基本型別的包裝類和String類都實現了自然排序,所以集合存放這些元素時輸出會預設採取自然排序
而如果存放自定義物件,就需要自己在類中實現介面,重寫方法,例如:一個簡單的學生類
class Student implements Comparable<Student> {//實現介面,注意泛型
private String name;
private int age;
// 一定要重寫hashCode與equals方法才能比較出成員變數是否相同
...中間程式碼省略
@Override
public int compareTo(Student o) {//重寫方法
// TODO Auto-generated method stub
//根據自己想要的規則進行重寫
int num = this.age - o.age;//這樣可以根據年齡排序
//但是依舊不夠完善,如果年齡一樣,會無法新增,怎麼辦
//分析次要條件,年齡一樣時,再比較姓名是否一致
int num2 = num==0?this.name.compareTo(o.name):num;//直接使用String類中compareTo方法比較
return num2;//正負可以控制正序還是逆序輸出
}
}
方式二:排序器排序
TreeSet構造方法中,可以傳入一個排序器作為引數,最後輸出也是按照排序器的規則進行。
通過Comparator介面,採用匿名內部類的方式,直接重寫compare方法,定義自己的規則
例如:我有一個Person類,有兩個屬性,name和age,定義規則為:按名字長度排序
TreeSet<Person> set = new TreeSet<Person>(new Comparator<Person>() {
public int compare(Person o1, Person o2) {
int num = o1.getName().length()-o2.getName().length();//比較名字長度
int num2 = num==0?o1.getAge()-o2.getAge():num;//要考慮主要的規則如果相同,次要規則怎麼寫
int num3 = num2==0?o1.getName().compareTo(o2.getName()):num2;
return num3;//如果所有規則都返回0,則可以說明元素相同,就不進行新增
}
});
其實就是將自然排序中實現介面與重寫方法單獨的拿出來當做引數傳入,原理是相同的
15、Map集合
(1)與Collection的比較
Map集合不在Collection集合的繼承體系中
實際開發中,例如學生類,我們存放的不應該只是每個學生的物件,還會有學號進行管理,獲取學生資訊時,大多是通過學號來進行的,但如果學號直接定義在學生類中,就意味著能拿到學號肯定也能直接拿到學生其他屬性,學號就沒有了存在的意義,所以Collection這樣的單列集合很顯然不滿足我們的需求,Java提供了一種雙列的集合,可以存放鍵值對的物件,能夠將學號與學生的每個物件一一對應。
Map集合最大的特點就在於這個鍵值對,元素是成對出現的,資料結構僅針對鍵有效,跟值無關,鍵是唯一的,而值可以重複
Collection元素時單獨出現的,資料結構針對每個元素有效,並有不同分支來存放是否有序和是否唯一的元素
(2)方法
put(key,value);新增對映關係,同時也是修改,當鍵存在時,值被替換
clear();清除所有對映關係,清空集合
remove(key);移除指定鍵的對映,返回對應的值,若鍵不存在,返回null
containsKey();判斷是否包含指定鍵,也有containsValue判斷是否含有值
get(key);根據鍵獲取值
keySet();獲取集合中鍵的集合,返回一個set集合,保證了元素唯一
value();獲取集合中值的集合,返回一個collection集合,保證值可重複
entrySet();返回對映中包含對映關係的set檢視
(3)遍歷
方式一:keySet方法獲取鍵的集合,通過get方法用鍵找值
Map<String, String> m = new HashMap<String, String>();
m.put("1", "hello");
m.put("2", "world");
m.put("3", "Java");
// 獲取所有鍵,遍歷鍵集合,根據鍵找值
Set<String> s = m.keySet();
for (String key : s) {
System.out.println(key + "---" + m.get(key));
}
方式二:entrySet獲取每個對映,遍歷鍵值對集合,分別找到鍵和值
// 獲取鍵值對物件,遍歷鍵值對集合,找到鍵和值
Set<Map.Entry<String, String>> set = m.entrySet();
for (Map.Entry<String, String> map : set) {
System.out.println(map.getKey() + "---" + map.getValue());
}
16、HashMap
(1)Map的實現類
基於雜湊表的Map介面實現類,雜湊表結構保證了元素唯一
理解Map之後,之前的例子也基本都是通過HashMap作為實現類來做,沒什麼特殊說明
(2)注意事項
如果使用自定義物件作為鍵值,與HashSet一樣要注意重寫自定義物件的hashCode和equals方法保證唯一性
(3)子類LinkedHashMap
結合了連結串列與雜湊表結構,連結串列保證有序,雜湊保證唯一,但都是針對鍵來說的,與值無關
17、TreeMap
(1)Map的另一個子類
同樣根據TreeSet理解,紅黑樹結構,保證唯一性並且進行了排序
(2)排序
一樣是兩個方式,自然排序和比較器排序,跟TreeSet原理相同,只注意這個排序是針對鍵即可
一個例子,學生類物件作為鍵,根據名字進行排序,採用匿名內部類傳遞比較器引數
TreeMap<Student, String> map = new TreeMap<Student, String>(new Comparator<Student>() {
public int compare(Student s1, Student s2) {
int num = s1.getName().compareTo(s2.getName());//根據名字排序
int num2 = num == 0 ? s1.getAge() - s2.getAge() : num;//注意次要條件,同名按照年齡再進行排序
return num2;//返回0則表示鍵相同,put時變為替換值
}
});
18、一些案例練習:
(1)輸入一串字元,統計各字元出現的次數
思路:定義一個Map集合,將得到的字串轉換為陣列,遍歷該陣列拿到每個字元去集合中找,找不到則將其定義為鍵,把對應值設定為1,找到則將對應值加一,這樣遍歷結束後Map中就存放了每個字元和對應的出現次數,並且為了保證按照字母的自然順序進行結果輸出,採用TreeMap進行存放
Scanner sc = new Scanner(System.in);
String line = sc.nextLine();//獲取鍵盤錄入字串
TreeMap<Character, Integer> map = new TreeMap<Character, Integer>();
char[] arr = line.toCharArray();//轉為陣列並遍歷
for (char ch : arr) {
Integer i = map.get(ch);//通過鍵找值
if (i == null) {
map.put(ch, 1);//找不到則新增這個對映,計數1
} else {
i++;
map.put(ch, i);//找到值+1後put修改
}
}
StringBuilder sb = new StringBuilder();
Set<Character> set = map.keySet();//獲取鍵集合
for (Character key : set) {//增強for遍歷
sb.append(key).append("(").append(map.get(key)).append(")");//按照一定的格式輸出
}
System.out.println(sb);
(2)Hashtable問題
Hashtable也是Map介面下雜湊結構的子實現類
可以結合ArrayList與Vector來理解
Hashtable就相當於Vector,HashMap就相當於ArrayList
Hashtable鍵或值不能為null,而HashMap可以
Hashtable是執行緒安全,效率低,HashMap執行緒不安全,效率高
除了這兩個區別,其他使用基本相同,同ArrayList理解相同,基本上HashMap是Hashtable的替代品
(3)集合繼承的混淆
面試可能經常會問List、Set、Map是否都繼承自同一個介面
如果不夠熟悉,加上他們都是集合框架,就容易弄混
List、Set是Collection下的介面,Map本身就是頂層介面
19、Collections工具類
(1)針對集合操作的工具類
會經常被問到與Collection的區別,他們完全不同,一個是工具類,負責集合的操作;另一個是一個頂層集合介面
(2)方法
sort(List<T> list);預設情況下進行自然排序,如果集合存放自定義集合,傳入自定義比較器作為第二個引數,當然也可以通過自定義類中實現comparable介面重寫compareTo方法實現
binary(List<T> list);二分查詢,注意二分查詢前必須先進行排序
reverse(list);反轉順序
shuffle(list);隨機洗牌
這些方法都是要List分支下子類才能使用,因為都需要有序才可以進行
max(c);獲取最大值,也有獲取最小值方法,集合都可以使用
20、集合框架總結
(1)單列集合Collection
List:元素有序,可重複,三個常見子類
ArrayList:底層資料結構為陣列,查詢快,增刪慢,執行緒不安全,效率高
Vector:底層資料結構為陣列,查詢快,增刪慢,執行緒安全,效率高
LinkedList:底層資料結構為連結串列,查詢慢,增刪快,執行緒不安全,效率高
Set:元素不保證有序,但唯一,兩個常見子類
HashSet:底層資料結構為雜湊表,依賴於hashCode與equals方法保證唯一;有一個子類LinkedHashSet,結合連結串列結構,實現了set集合的有序。
TreeSet:底層資料結構為紅黑樹,元素唯一併進行排序,預設進行自然排序,通過類實現comparable介面並重寫compareTo方法,也可自定義排序器,通過匿名內部類重寫compare方法實現。
(2)雙列集合Map
Map:資料結構僅針對鍵有效,與值無關;鍵唯一,值可重複,兩個常用子類
HashMap:雜湊表結構,保證鍵唯一;有一個子類LinkedHashMap,通過連結串列結構實現了有序。
Hashtable:不常用子類,除了鍵值不能為null,其他用法與HashMap相同,並且執行緒安全,效率低,基本被代替
TreeMap:紅黑樹結構,通過compare保證鍵唯一和排序
(3)注意事項和問題
集合框架中的有序,都只是指傳入和取出的順序一致,而不是排序的順序
集合框架的選擇需要考慮需求:單列還是雙列、是否需要有序,是否需要元素唯一、是否需要排序、執行緒安全與否、增刪多還是查詢多等等。使用較多的有:ArrayList 、HashSet 、HashMap
二、IO流
1、異常
(1)引入
IO流會產生很多異常,所以先要了解什麼是異常
就是程式出現的不正常的情況
(2)分類
Throwable是所有問題和異常的超類
嚴重問題Error,不能進行處理,需要修改程式碼
異常問題Exception,有執行時異常,編譯期間不報錯,但執行起來就會出問題,這類異常一般也不進行處理,因為大多數因為自己的程式碼不夠研究;除了執行時異常意外的異常,就是編譯期異常,需要進行處理。
(3)處理異常方式
方式一:try...catch...finally塊處理異常
注意事項:
原則上來說,try內的程式碼越少越好
catch中必須寫內容,不處理也至少是資訊的輸出
多個catch塊時,一個塊捕捉到異常,則後面catch不再執行
平級異常順序可以隨意,但如果異常由繼承關係,父類要在子類的後面catch
格式上,必須有try,catch和finally必有其一,catch可以有多個,finally只有一個
方式二:throws關鍵字丟擲異常
在異常不能處理或不想處理,丟擲異常讓使用方法的人去處理,平時測試中會節省一些編寫時間
注意事項:
丟擲執行時異常,可以不進行處理
可以丟擲多個異常,用逗號隔開
與throw關鍵字的區別
throws用在方法聲明後,跟異常類名,可以有多個,逗號隔開;表示丟擲異常,由方法呼叫者處理,是一種異常發生的可能性,而不是一定會發生
throw用在具體方法體內,後面跟異常物件,只能丟擲一個;表示程式碼走到這裡一定會丟擲這個異常
(4)特別說一下finally關鍵字
在異常處理塊中最後的位置,被finally控制的語句體不管是否發生異常都會執行(除非異常處理中使用了exit方法關閉了虛擬機器),虛擬機器關閉了則不執行。
與final和finalize的區別
final是修飾符,修飾類時表示不能被繼承,修飾變數則是變為常量,修飾方法不能被重寫
finally是異常處理中的一部分,可用於釋放資源,一般情況下不管異常是否發生都會執行
finalize是Object中方法,用於垃圾回收。
finally與return的執行順序問題
finally之前如果有return語句,finally依然會執行,但會在return前執行,不過這裡其實只是這樣說。真正的執行順序,應該理解為,return拆分成兩次,finally在兩次之間執行,例子:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;//結果為30
} finally {
a = 40;
}
return a;
}
分析:除以0之後,捕捉到異常,進入catch塊,a賦值為30後返回了30,這裡其實已經形成了一種類似對映的返回路徑,相當於形成了return 30;這個語句,但finally要執行,a的值確實變為了40,但再次返回return時,還是走原來形成的返回路徑,所以結果還是30.
(5)異常中的繼承問題
子類只能丟擲父類中相同的異常或其子類異常,不能丟擲父類沒有的異常、如果父類中一個異常都沒有,子類只能自己用try...catch塊處理,不能丟擲。
2、File類
(1)IO包下的一個類
是檔案和目錄的抽象表達形式
(2)方法
creatNewFile();建立檔案,返回true
mkdir();建立資料夾,如果存在則返回false
mkdirs();建立多級資料夾
delete();刪除檔案或目錄,注意不走回收站
renameTo(File dest);重新命名,需要file路徑相同,如果路徑不同,就相當於進行了剪下
方法總結似乎沒什麼用,自己看API就好,後面可能會簡寫了
有一些判斷功能:是否是目錄,是否是檔案,是否存在等
獲取功能:獲取絕對路徑、相對路徑,獲取檔名,獲取位元組數,獲取最後一次修改時間
高階獲取:listFile();獲取目錄下所有檔案和目錄的File物件陣列
(3)過濾器介面
FilenameFilter檔名過濾器
list和listFile方法都可以以匿名內部類傳入一個filter物件,例如想獲取一個目錄下txt字尾的檔案:
String[] list = file.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
// TODO Auto-generated method stub
// 自己限定如何才返回true
// File file = new File(dir, name);
// boolean flag = file.isFile() && name.endsWith(".txt");// 是檔案才加入list
// return flag;
//簡潔寫法
return new File(dir,name).isFile() && name.endsWith(".txt");
}
});
3、遞迴
(1)引入
由於一個目錄下,會有目錄和檔案,還會有多級目錄,所以為了能夠迴圈的去判斷和操作,需要使用遞迴
(2)注意事項
遞迴一定要有出口,否則是死迴圈
遞迴次數不能太多否則會記憶體溢位
構造方法不能遞迴
(3)簡單使用
經典的求階乘
public static int getJc(int n) {
if (n == 1) {
return 1;
} else {
return n * getJc(n - 1);
}
}
斐波那契問題:一串數字,從第三個數開始每個是前兩個數相加
// 遞迴實現 返回 型別int,引數int n
// 出口 第一二月是1
public static int fib(int n) {
if (n == 1 | n == 2) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
輸出指定目錄下所有java檔案絕對路徑
public static void getJava(File file) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isFile()) {
if (f.getName().endsWith(".java")) {
System.out.println(f.getAbsolutePath());
}
} else if (f.isDirectory()) {
getJava(f);
}
}
}
4、位元組流
(1)抽象基類
InputStream、OutputStream
(2)輸出流
FileOutputStream輸出步驟
建立位元組輸出流物件
寫資料(向檔案中寫資料,每次都重新寫入,傳入true作為第二個引數則為追加寫入)
釋放資源
(3)輸入流
FileInputStream讀入步驟
建立位元組輸入流物件
讀資料(可以一次讀一個位元組,到達檔案末尾返回-1,也可以一次讀一個位元組陣列,陣列長度一般設為1024的倍數)
關閉流
(4)緩衝流
利用了緩衝區,將位元組輸入輸出流進行包裝,可以進行高效傳輸
BufferedInputStream和BufferedOutputStream
不能直接對檔案進行操作,因為只是一種高效的包裝,還是需要傳入位元組流物件
這其實是一種裝飾設計模式,最後再進行總結
5、字元流
(1)引入轉換流
位元組流在文字檔案傳輸中,讀出的資料可讀性較差,所以針對一些中文的文字檔案,Java提供了字元流
字元流其實是位元組流加上編碼表,所以需要一個轉化流,實現位元組流到字元流的轉換
它是兩個流的橋樑,可以指定編碼和解碼的字符集,預設使用平臺的預設字符集
讀寫方法與位元組流相同,但用的是字元和字元陣列作為讀寫中介
(2)注意事項
位元組流寫入資料時,即使沒有關閉流,資料也寫入了
而字元流執行完寫入操作,只是建立了檔案
一個字元是兩個位元組(中文一般是三個負數),而檔案中儲存的基本單位是位元組
所以執行完寫入,寫的字元都在緩衝區內
需要用flush()方法重新整理一次緩衝區,才能寫入資料
而close方法是關閉流,但關閉前其實內建了一次重新整理方法
flush方法使用後流還能繼續操作,close之後流物件不能再使用
(3)便攜字元流
除了通過位元組流轉換為字元流,Java還提供了能直接操作檔案的便攜字元流
FileReader和FileWriter
(4)高效字元流
與位元組流有高效流對應的,字元流也有高效緩衝流
BufferedReader和BufferedWriter,同樣只是包裝,不能直接操作檔案,需要傳入普通字元流物件
很方便的是,這個流可以直接讀取檔案的一行,寫入時也有newLine方法可以換行
6、小結
/*
* IO流的位元組與字元流小結
*
* 位元組流
* 位元組輸入流InputStream--FileInputStream
* |--filter--BufferedInputStream傳入位元組流物件
* 位元組輸出流OutputStream--FileOutputStream
* |--filter--BufferedOutputStream
* 讀寫方法:read()/read(byte[] b)
* write(int content)/write(byte[] b,int off,int len)
*
* 轉換流:
* 輸入:InputStreamReader傳入InputStream物件
* 輸出:OutputStreamWriter傳入OutputStream物件
* 實際傳入的為位元組流兩個子類物件
*
* 字元流
* 字元輸入流Reader--InputStreamReader--FileReader
* |--BufferedReader傳入字元流物件
* 位元組輸出流Writer--OutputStreamWriter--FileWriter
* |--BufferedWriter
* 讀寫方法:read()/read(char[] c)
* write(int ch)/write(char[] c,int off,int len)
* 還可以直接寫字串write(String str)
* 特殊讀寫方法:readLine()
* newLine()
*
*/
一些綜合案例:
(1)將一個資料夾複製到指定目錄下
public static void main(String[] args) throws IOException {
//資料來源
File src = new File("源路徑");
//目的地
File dest = new File("目標路徑");
copy(src,dest);
}
public static void copy (File src,File dest) throws IOException {
//判斷是否是資料夾
if(src.isDirectory()) {
//是資料夾先在目的地建立資料夾
File newFile = new File(dest,src.getName());
newFile.mkdir();
//獲取原始檔夾下所有檔案
File[] fileList = src.listFiles();
//遍歷遞迴
for(File file : fileList) {
copy(file,newFile);
}
} else {
//如果是檔案,則複製檔案
File newFile = new File(dest,src.getName());
copyFile(src,newFile);//位元組流讀寫檔案方式
}
}
private static void copyFile(File src, File newFile) throws IOException {
// TODO Auto-generated method stub
BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream out= new BufferedOutputStream(new FileOutputStream(newFile));
byte[] b = new byte[1024];
int len;
while((len=in.read(b))!=-1) {
out.write(b, 0, len);
}
in.close();
out.close();
}
(2)自定義類模擬BufferedReader的rendLine()方法
class MyBufferedReader {
private Reader reader;
public MyBufferedReader(Reader r) {
reader = r;
}
public String readLine() throws IOException {
StringBuilder sb = new StringBuilder();
int ch;
while ((ch = reader.read()) != -1) {
if (ch == '\r') {
continue;
}
if (ch == '\n') {
return sb.toString();
} else {
sb.append((char)ch);
}
}
//防止資料丟失
if(sb.length()>0) {
return sb.toString();
}
return null;
}
public void close() throws IOException {
reader.close();
}
}
注意事項是有可能讀取最後一行時,資料已經讀到了緩衝區,但沒有換行符,用來讀取的位元組返回了-1,無法通過換行符裡的方法返回字串並跳出迴圈,那麼最後一行的資料就可能丟失,所以在迴圈外層最後加一個判斷,如果緩衝區裡還有字元,也進行一次return,這樣就保證了資料完整
(3)自定義類模擬LineNumberReader獲取行號的方法
class MyLineNumberReader {
private Reader reader;
private int lineNumber = 0;
public int getLineNumber() {
return lineNumber;
}
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
...省略了與上個例子相同的程式碼
public String readLine() throws IOException {
lineNumber++;
...程式碼省略
}
}
思路是定義一個行號的成員變數,在每次執行readLine方法時,將行號+1,設定和獲取就是普通的get/set方法。
7、其他流的簡單介紹
(1)針對基本資料型別的資料輸入輸出流DataInputStream和DataOutputStream
(2)記憶體操作流:將資料寫入緩衝區,是暫時的資料,不需要關閉
操作位元組ByteArrayInputStream和ByteArrayOutputStream
操作字元CharArrayReader和CharArrayWriter
操作字串StringReader和StringWriter
(3)列印流:PrintStream和PrintWriter
有print()方法可以輸出任何型別資料,println可以自動換行
構造方法可以傳入第二個引數true開啟自動重新整理
(4)標準輸入輸出流:System類中的in和out欄位
代表了輸入輸出裝置,預設是鍵盤和顯示器
System.in型別是InputStream
System.out型別是PrintStream,所以可以呼叫println方法,這就解釋了最常用輸出語句的原理
鍵盤錄入資料的幾種實現方式:
main方法中接收的args引數,最早期命令列介面傳引數
Scanner物件傳入System.in輸入流
通過轉換流包裝System.in,再包裝為緩衝流,利用readLine方法讀取(輸出流也可以這樣包裝)
(5)隨機訪問流RandomAccessFile
繼承於Object類,不屬於流但融合了流的輸入輸出功能
建構函式傳入兩個引數,第一個為檔案路徑,第二個是許可權模式(結合linux許可權“rwd”理解)
(6)合併流SequenceInputStream可以將兩個或多個InputStream輸入流同時讀取
(7)序列化流與反序列化流
ObjectOutputStream和ObjectInputStream在流中對物件進行讀寫
物件類需要實現Serializable介面,啟用序列化功能,該介面沒有任何方法,此類介面被稱為標記介面
注意事項:
在進行了一次序列化,儲存到檔案中之後,如果修改了類,會丟擲一個異常InvalidClassException
這是因為此類實現了序列化之後,系統會預設給一個標記值serialVersionUID,修改之後這個標記值就改變了
只有重新儲存之後,才能匹配;但實際中,不可能每次修改類都重新寫入資料,可以自己限定這個UID
一般點選實現了介面後產生的黃色警告線讓他自己生成一個固定的標記值即可
還有一個問題就是,不是所有成員屬性都想被序列化,那麼要用transient修飾這樣的成員變數
8、Properties集合
(1)為什麼要放在IO流裡
他是Hashtable的子類,是屬性集合類,可以和IO流集合使用
(2)特點
可以儲存在流中或從流中讀出鍵值對,每個鍵值對都是字串,所以不使用泛型,雙列都是字串
集合中新增對映最好不要用put方法,因為有可能傳入其他型別,應該用setProperty方法
因為不安全的properties集合物件(包含非String鍵值)中很多方法都會失效
(3)與流的結合使用
load(Reader reader)把檔案中的鍵值對資料讀入到集合中
store(Writer writer,String comments)把集合中資料儲存到檔案中,第二個引數是註釋資訊
9、NIO
jdk4出現的NIO使用了不同方式處理輸入輸出,採用了記憶體對映檔案的方式,將檔案或者檔案的一段區域對映到記憶體中,像訪問記憶體一樣訪問檔案,這種方式比普通IO快很多,有Buffer和Channer類
jdk7下有一個介面Path,表示與平臺無關的路徑
工具類Paths,有一個get方法根據URI返回檔案路徑物件
工具類Files,copy方法可以直接複製檔案,只需要傳入原始檔路徑物件和輸出流;write方法可以將集合資料寫入檔案。
NIO就是jdk升級中,將很多IO的操作變得更簡單
三、多執行緒
1、執行緒
(1)程序
要了解執行緒,就要先了解程序,執行緒依賴於程序而存在
程序就是正在執行的程式,是系統進行資源分配和呼叫的獨立單位
每一個程序都有自己的記憶體空間和系統資源
多執行緒的意義在於同一時間執行多個程式,可以同時做很多事,提高資源的利用率
但只是看起來是同一時間執行,單核cpu的一個時間點其實只能做一件事,是在多個程序間高速切換
(2)執行緒
一個程序中可以執行多個任務,每個任務就可以看成一個執行緒
執行緒是一個程式的執行單元,執行路徑;是程式使用cpu的最基本單位
有多個執行路徑的就是多執行緒程式,程式的執行其實是對cpu資源的搶奪
所以多執行緒的意義在於提高應用的使用率,多程序中哪個程序的執行緒比較多就更有可能有更高的執行權
但並不能保證,因為執行緒的執行有隨機性
(多執行緒和多程序其實都不是提高效率,而是提高資源利用率)
(3)並行和併發
並行是邏輯上同時發生,指某個時間內同時執行多個程式
併發是物理上同時發生,指某個時間點同時執行多個程式
(4)Java程式的執行原理
啟動Jvm,相當於啟動了一個程序,然後程式會啟動主執行緒呼叫main方法,但其實它的啟動應該是多執行緒的,因為除了一個主執行緒,還應該至少有一個垃圾回收執行緒
(5)多執行緒的實現方式
方式一:繼承Thread類,重寫run()方法;建立執行緒物件,啟動執行緒
啟動執行緒中的問題:run和start的區別
run是封裝線上程中被執行的程式碼,直接呼叫就跟普通方法呼叫沒有區別
start則是啟動這個執行緒物件,再由JVM去隨機的呼叫各執行緒run方法
方式二:實現Runnable介面,重寫run方法;建立自定義類物件,使用Thread帶參構造傳入這個物件,啟動執行緒
這個方式的優勢在於,很多時候我們的類本身需要有繼承關係,而java沒有多繼承
實現介面就避免了這個限制,並且他把執行緒同程式程式碼、資料有效分離,較好的體現了面向物件思想
只需要建立一個物件,就可以開啟多個執行緒,多個相同的程式碼處理同一資源,更符合多執行緒的要求
(6)執行緒類的方法
執行緒排程和優先順序:java用的是一種搶佔式排程
setPriority設定優先順序,預設是5,範圍是1-10。超過範圍報異常IllegalArgumentException
執行緒控制:sleep,在指定毫秒內讓執行緒暫停
join,等待該執行緒終止才執行其他執行緒
yield,禮讓性暫停該執行緒,執行其他執行緒,但並不能保證一定禮讓,只是更和諧
setDeamon,將該執行緒標記為使用者執行緒,也稱後臺執行緒或守護執行緒,當正在執行的執行緒都是守護執行緒時,jvm退出,該方法的呼叫必須在啟動執行緒前
stop,停止執行緒,不安全,已經過時因為後面的程式碼無法執行
interrupt,中斷執行緒,把執行緒狀態終止,丟擲異常,後面的程式碼還能夠執行
(7)執行緒生命週期
2、多執行緒中產生的問題
(1)同步問題
經典的多視窗賣票案例中,三個執行緒同時處理一個數據,即使限定了迴圈的跳出條件,但cpu的每次操作是原子性操作,類似票數--,這樣的操作實際上並不是一步完成的,所以可能會出現多執行緒都能進入迴圈,出迴圈票數變負、或者同票賣多次的情況。這就是一種執行緒不安全的問題。
一般出現的原因有:多執行緒,共享資料,多條語句共同操作共享資料。
而這三個原因,前兩個就是多執行緒的需求,不能通過更改他們來解決問題,所以著手於第三點,讓這一塊程式碼執行中,其他執行緒不能執行;java對此提供了同步機制。
同步程式碼塊的關鍵字synchronized,格式是小括號里加鎖物件,大括號內放需要同步的程式碼;同步解決安全問題的根本原因在於該鎖物件上,可以在類中隨意宣告一個Object物件做為鎖物件,但多個執行緒應該用同一把鎖。
同步的特點:前提是多執行緒,且共用一把鎖;好處是解決了多執行緒中的安全問題;弊端是當執行緒很多時,都需要去判斷同步中正在使用的鎖,很耗資源,降低了執行效率。
當想把一個方法定為為同步的,則在宣告中加入synchronized關鍵字即可,但這時候,他的鎖物件是this;如果這個方法是靜態方法呢,本身this物件還沒有被建立,方法隨類的初始化而載入,其實此時鎖物件是類的位元組碼檔案本身,這是一種反射,後面還會總結。
(之前提過的Vector,雖然他是List集合下常用三個子類中唯一執行緒同步的集合,但是當我們需要使用執行緒安全的集合時,也不會去使用他,而是使用Collections工具類中的方法,將一個其他集合設定為同步集合來使用:List<String> synList = Collections.synchronizedList(new ArrayList<String>());這樣就可以執行緒安全的使用ArrayList了)
Jdk5以後,提供了一個Lock介面,用具體實現類ReentrantLock建立一個物件,分別在需要上鎖的地方和解鎖的地方使用方法lock(),unlock()就可以實現同步了,更清晰的表達了加鎖和釋放鎖。
(2)死鎖問題
同步的弊端除了降低了效率,還有一個風險,如果出現了同步巢狀,容易產生死鎖問題,指的是在多個執行緒執行過程中,因互相爭奪資源而產生的互相等待現象;例如有兩把鎖,兩邊分別進入了一把鎖之後,都需要對方解鎖後才能繼續執行,互相都在等待對方出來,一直僵持。寫一個簡單的死鎖案例:
public class Test {
public static void main(String[] args) {
Thread td1 = new Thread(new DeadLock(true));
Thread td2 = new Thread(new DeadLock(false));
td1.start();
td2.start();//只有理想狀態下,這兩個才能順利執行完畢
}
}
//定義兩把鎖
class MyLock {
public static final Object lock1 = new Object();
public static final Object lock2 = new Object();
}
//一個死鎖案例
class DeadLock implements Runnable {
private boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//當兩個狀態不同的執行緒,走這一段程式碼,分兩條線走
//就可能出現互相等待對方使用的鎖又都出不來的情況,即死鎖
if (flag) {
synchronized (MyLock.lock1) {
System.out.println();
synchronized (MyLock.lock2) {
System.out.println();
}
}
} else {
synchronized (MyLock.lock2) {
System.out.println();
synchronized (MyLock.lock1) {
System.out.println();
}
}
}
}
}
(3)生產者與消費者的問題
不同種類執行緒針對同一資源操作,由於執行緒隨機性,可能會出現生產者還沒生產,消費者就索取,同樣也是執行緒安全問題,解決方法也是加鎖,要注意的是不同種類執行緒加同一把鎖。
但這個問題中,雖然加鎖解決了同步,但還會有其他情況出現,由於執行緒排程的隨機性,消費者先搶到執行權,但生產者還沒生產,資料還處在預設值,就應該通知生產者,並且等待生產完畢再消費;如果生產者搶到執行權,生產完畢後,如果還有執行權,就應該通知消費者來使用,等消費完再生產,而不是一直生產。
對此,java中提供了等待喚醒機制,Object類中有三個方法:wait()/notify()/notifyAll().而這三個方法為什麼要定義在Object中,原因是這些方法需要同步中的鎖物件來進行呼叫,而在同步程式碼塊中,這個鎖物件可以是任意物件,所以相容所有物件的Object可以完成這個任務。一個簡單的例子:
public class Test2 {
public static void main(String[] args) {
Student s = new Student();
Thread td1 = new Thread(new setStudent(s));
Thread td2 = new Thread(new getStudent(s));
td1.start();
td2.start();
}
}
// 學生類
class Student {
// 方便操作資料
String name;
int age;
// 加一個標記
boolean flag;// 預設是false,代表沒資料
}
//生產者
class setStudent implements Runnable {
Student s;
int x;
public setStudent(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (s.flag) {
// 如果有資料,生產者等待資料被消費
try {
s.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.name = "學生A";
s.age = 23;
} else {
s.name = "學生B";
s.age = 22;
}
x++;
// 生產完畢,修改標記,喚醒消費者
s.flag = true;
s.notify();
}
}
}
}
//消費者
class getStudent implements Runnable {
Student s;
public getStudent(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (!s.flag) {
// 沒有資料,消費者就等待
try {
s.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(s.name + "---" + s.age);
// 消費完畢,修改標記,喚醒生產者
s.flag = false;
s.notify();
}
}
}
}
3、執行緒組
可以使用ThreadGroup表示執行緒組,可以對一批執行緒進行分類管理
預設情況下,所有執行緒屬於主執行緒組
給執行緒設定分組可用Thread類中一個構造方法Thread(ThreadGroup group,Runnable target,String name)
4、執行緒池
啟動新執行緒的成本較高,因為涉及到與系統功能的互動,而使用執行緒池可以很好的提高效能,尤其是要建立大量生存週期短的執行緒時,更要考慮執行緒池;池中每個執行緒程式碼結束後,不會死亡,而是回到池中成為空閒狀態,等待下一個物件來使用。
* Executors工廠類產生執行緒池
* newCachedThreadPool()空執行緒池
* newFixedThreadPool(int nThreads)建立存放n個執行緒的執行緒池
* newSingleThreadExecutor()存放一個執行緒的池
* 這些方法返回一個ExecutorService物件
* 這種執行緒池可以執行Runnable物件或Callable物件代表的執行緒
*
* 執行緒池使用過程:
* 靜態方法傳入想要管理的執行緒數量
* 定義一個實現了Runnable介面的類
* 呼叫方法Future<?> submit(Runnable task)
* 要結束執行緒池,用shutdown方法
這裡提到了第三種實現執行緒的方式,依賴於執行緒池,可以使用泛型,所以不經常提:就是類實現Callable介面重寫call方法。
5、匿名內部類方式使用執行緒
如果執行緒中程式碼較少,使用次數不多,其實可以不用專門定義一個類去繼承或實現介面,可以直接通過匿名內部類,重寫Thread中的run方法,或傳入介面子實現物件,同樣重寫run方法。要注意的是,如果同時重寫了兩個run,執行時是走Thread中的方法。例如:
// Thread類
new Thread() {
@Override
public void run() {
System.out.println("helloworld");
}
}.start();
// Runnable介面
new Thread(new Runnable() {
public void run() {
System.out.println("helloworld");
}
}) {
}.start();
// 兩個大括號中都重寫run呢?
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("helloworld");
}
}) {
@Override
public void run() {
System.out.println("helloJava");
//執行緒執行走這裡的run
}
}.start();
6、Timer定時器
應用廣泛的執行緒工具,可用於排程多個定時任務以後臺執行緒方式執行,但開發中其實是用其他更好的開源框架。
簡單使用:一個Timer物件,一個TimerTask任務,通過schedule方法傳相應引數設定任務時間。
一個定時刪除指定資料夾的例子,應用了遞迴和定時任務:
public class Test1 {
public static void main(String[] args) {
Timer t = new Timer();
DeleteFiles task = new DeleteFiles();
t.schedule(task, 1000);
}
}
class DeleteFiles extends TimerTask {
@Override
public void run() {
File file = new File("指定目錄");
delete(file);
}
public void delete (File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
for(File f : files) {
delete(f);
}
} else {
file.delete();
}
//刪完檔案刪資料夾
file.delete();
}
}
更好的改進是,構造方法傳入檔案物件。這裡只是簡單測試
7、總結
(1)多執行緒實現方式:繼承Thread類或實現Runnable介面,重寫run方法。(Callable瞭解)
(2)synchronized關鍵字實現同步,注意鎖物件的使用
(3)注意run和start方法區別,一個是普通呼叫,另一個才是啟動執行緒
(4)sleep與wait區別:前者必須指定時間,並且睡眠期間不會釋放鎖;後者不指定時間,用notify喚醒,等待期間會釋放鎖。
(5)為什麼notify、wait、notifyAll定義在Object類中:鎖物件的隨意性。
(6)執行緒生命週期:新建、就緒、執行、死亡。執行中可能因為各種原因發生阻塞。
四、網路程式設計
1、網路相關知識
* 網路模型:計算機網路之間以何種規則進行通訊
* OSI:open system interconnection開放系統互聯
* TCP/IP參考模型
*
* 七層網路結構
* 應用-表示-會話-傳輸-網路-資料鏈路-物理
*
* 網路應用程式組成:網路程式設計、IO流、多執行緒(還應該有集合)
*
* 網路程式設計三要素
* IP地址
* 網路中計算機的唯一標識,用點分十進位制技術表示
* IP組成:網路號段+主機號段
* 分類:A-E五類,一般是C類,前三段為網路號+後一段主機號,一個網路號256個
* 其中一般192.168.x.x和10.x.x.x為私有地址,配置區域網用
* 常用dos命令:ipconfig、ping
* 特殊地址:127.0.0.1為本機(迴環地址)x.x.x.255廣播地址 x.x.x.0網路地址
* 埠
* 這裡指的是邏輯埠,每個網路程式都至少有一個邏輯埠
* 是用於標識程序的邏輯地址,不同程序的標識
* 有效埠:0-65535,其中0-1024為系統使用或保留埠
* 協議
* 通訊的規則
* UDP:資料打包,大小限制,不需要建立連線,不可靠,但速度快
* TCP:通過三次握手建立連線,可進行大資料量傳輸,可靠,但效率稍低
* 一般軟體都是兩種協議都有,tcp保證安全,udp保證速度
* 舉例:發簡訊、qq、微信發訊息為udp,需要接聽的電話為tcp
2、前提知識
(1)InetAddress類
java中表示IP的類,沒有構造方法,有一個方法返回該類物件getByName(String name),根據主機名或IP獲取物件
(2)Socket套接字
網路上具有唯一標識的IP地址和埠號組合在一起,構成能夠唯一識別的識別符號套接字
通訊的兩端都會有socket,網路通訊的其實就是資料在socket之間通過IO流傳輸
3、UDP協議
(1)不需要連線
資料打包,大小有限制,不可靠但速度快;不保證投遞,傳送端可以直接執行
(2)傳送資料的步驟
建立傳送端Socket物件
建立資料,打包資料
呼叫socket物件方法傳送資料包
釋放資源
* UDP協議用DatagramSocket類,傳送和接收端都使用此物件
* 有一個send方法用於傳送資料,傳入DatagramPacket物件
* DatagramPacket類表示資料包,實現無連線包投遞服務,不對投遞做出保證
* 包含資訊有資料、其長度、遠端主機IP和埠號,可通過構造方法傳遞
(3)接收端
建立接收端Socket物件,提供埠
建立一個數據包用於接收資料
呼叫方法接收資料
解析資料
釋放資源
接收端執行之後,receive方法是一個阻塞式方法,直到接收到資料才會繼續向下執行。
(4)多執行緒實現控制檯的簡單聊天室程式
public class Test {
public static void main(String[] args) throws IOException {
DatagramSocket sendDs = new DatagramSocket();
DatagramSocket receiveDs = new DatagramSocket(10005);
SendSocket s = new SendSocket(sendDs);
ReceiveSocket r = new ReceiveSocket(receiveDs);
Thread td1 = new Thread(s);
Thread td2 = new Thread(r);
td1.start();
td2.start();
}
}
// 傳送端
class SendSocket implements Runnable {
private DatagramSocket ds = null;
public SendSocket (DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
try {
// 錄入資料
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String text = null;
while ((text = br.readLine()) != null) {
if ("88".equals(text)) {
break;
}
//包裝資料
byte[] b = text.getBytes();
DatagramPacket dp = new DatagramPacket(b,b.length,InetAddress.getByName("127.0.0.1"),10005);
ds.send(dp);
}
...省略異常處理程式碼
} finally {
if (ds != null) {
ds.close();
}
}
}
}
// 接收端
class ReceiveSocket implements Runnable {
private DatagramSocket ds = null;
public ReceiveSocket (DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
try {
while (true) {
// 建立接收資料包
byte[] b = new byte[1024];
DatagramPacket dp = new DatagramPacket(b, b.length);
ds.receive(dp);
// 解析資料
String s = new String(dp.getData(), 0, dp.getLength());
System.out.println(dp.getAddress().getHostName() + ":" + s);
}
}...省略異常處理塊
}
}
4、TCP協議(1)需要三次握手,必須進行連線
協議可靠,但效率較低
(2)傳送端-Client客戶端
建立Socket物件
獲取輸出流,寫資料
釋放資源
不能直接執行,要有接收端建立連線
(3)接收端-Sever
建立Socket物件
監聽客戶端,返回一個對應的socket物件,accept為阻塞式方法
獲取輸入流,讀資料
釋放資源
(4)上傳檔案例子
客戶端