Java 概要
Java環境搭建
開始學習Java , 往往需要配置Java的基礎環境, 環境配置好後就可以編寫Java代碼并編譯運作. 可以選擇用系統自帶的記事本打開編寫代碼, 用JDK編譯運作, 但是我使用的是Idea作為Java的IDE工具進行編譯并運作代碼
1. Java基礎環境搭建
所謂的Java基礎環境搭建簡單來說就是下載下傳對應的JDK, 安裝後配置對應的環境變量, 下面是基礎環境搭建的詳細流程.
- 首先通過 官網 下載下傳對應的JDK版本, 個人推薦使用JDK1.8 , 原因是很多教程針對JDK1.8, 我使用的是JDK13
- 下載下傳完成後進行"傻瓜式"安裝, 安裝完成後檢查或配置環境變量, 在計算機中找到環境變量設定的地方, 試着添加如下變量名和值
變量名 值 JAVA_HOME JAVA安裝目錄 CLASSPATH %JAVA_HOME%\lib\dt.jar %JAVA_HOME%\lib\tools.jar Path %JAVA_HOME%\bin %JAVA_HOMT%\jre\bin - JDK基礎環境安裝完畢, 下面通過指令行檢測是否安裝正确. 打開指令行, 輸入
, 輸出結果類似java -version
java version "15.0.2" 2021-01-19 Java(TM) SE Runtime Environment (build 15.0.2+7-27) Java HotSpot(TM) 64-Bit Server VM (build 15.0.2+7-27, mixed mode, sharing)
2. Idea安裝
IDE是每個從事程式設計工作的人必須接觸的工具, 一個好的IDE可以大大地提高研發效率, Idea就是這樣的一款工具.
Idea的安裝非常簡單, 通過如下幾步即可輕松完成:
1. 通過搜尋找到相關下載下傳位址或者[官網](https://www.jetbrains.com/idea/download/)下載下傳, 盡量選擇**Ultimate**版本下載下傳
2. 下載下傳後的檔案如為: `ideaIU-2021.2.1.win.zip`, 解壓此檔案到想安裝的目錄即可
3. 運作其中的`idea.exe`, 按照相關指引填寫資訊後即可進入程式
3. 第一個Java程式
Java的基礎環境與編譯運作環境已經準備妥當, 下面将運作第一個程式"Hello World!"
- 在idea中依次點選
, 建議project name設定為HelloWorldNew Project -> Java -> Next -> Next -> (填寫Project相關資訊後) -> Finish
- 如果左側沒有Project菜單, 可以點選左側Project呼出, 之後右鍵點選
, 依次選擇HelloWorld\src
, 添加Name為HelloWorldNew -> Java Class
- 現在, 已經建立了第一個Java的類 HelloWorld, 在此類中寫如下内容
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); } }
- 在此檔案上右鍵點選, 選擇
Run HelloWorld.main()
, 可在下方Console窗格中看到輸出: "Hello World!"
至此, 一個最簡單的Java程式編寫完成. 如果在編寫代碼中出現錯誤, 可以在檔案中滑鼠指向錯誤标記處點選紅色燈泡檢視建議. 這也是直接使用IDE的諸多好處之一
基本類型與運算
1. 基本類型概述
Java常用的類型包含表示真假的boolean, 表示字元的char, 表示數值的type, short, int, long, float, double, 表示空的 void, 詳細見下
基本類型 | 大小(bit) | 最小值 | 最大值 | 包裝器類型 | 預設值 |
---|---|---|---|---|---|
boolean | - | Boolean | false | ||
char | 16 | Unicode 0 | Unicode $2^{16}-1$ | Character | '\u0000'(null) |
byte | 8 | -128 | +127 | Byte | byte(0) |
short | $-2^{15}$ | $+2^{15}-1$ | Short | short(0) | |
int | 32 | $-2^{31}$ | $+2^{31}-1$ | Integer | |
long | 64 | $-2^{63}$ | $-2^{63}-1$ | Long | 0L |
float | $IEEE_{754}$ | Float | 0.0f | ||
double | Double | 0.0d | |||
void | Void |
2. 操作符
操作符用于進行變量或者對象之間的計算或關系判斷, 沒有操作符就無法做任何運算. 比較 或者指派. 操作符主要分為以下幾類, 分别是: 算術操作符, 指派操作符. 關系操作符, 邏輯操作符, 位操作符和其他操作符.
- 算術操作符 其中, 二進制運算可以與等号進行縮寫, 比如
描述 操作符 變量 demo 結果 加, 減, 乘, 除 +, -, *, / a = 1, b = 2 "a + b = " + (a + b) a + b = 3 取模 % a = 3, b = 2 "a % b = " + (a % b) a % b = 1 自增自減 ++, -- a = 1 a++; "a++ =" + a .sout a++ = 2
可以縮寫為a= a+b
a += b
-
指派操作符
從上面可以看到一個符号
, 它的目的是把右邊的值指派給左邊, 有些書把=
,+=
等也歸入指派運算符, 竊認為這更傾向于算術操作符與指派操作符的縮寫-=
-
關系操作符
主要包含六種, 見下表:
== 檢查左右兩側操作數是否相等, 相等為真 != 檢查左右兩側操作數是否不等, 不等為真 > 檢查左側操作數是否大于右側操作數, 大于為真 < 檢查左側操作數是否小于右側操作數, 小于為真 >= 檢查左側操作數是否不小于右側操作數, 不小于為真 <= 檢查左側操作數是否不大于右側操作數, 不大于為真 -
邏輯操作符
包含與操作符
與或操作符&&
||
-
位操作符
假設下表中的變量i與變量j分别為5和7, 類型為int
& 左右兩個操作數按位進行且操作 i&j 5 \ 左右兩個操作數按位進行或操作 i\ j 7 ^ 左右兩個操作數按位進行異或操作 i^j 2 ~ 對操作數按位取反 ~i -6 << 按位向左移動 i<<1 10 >> 按位向右移動 i>>1 - 其他操作符
三目運算符 通過第一個操作數判斷條件是否為真進而在後兩個操作數中執行一個 1==2?"1==2":"1!=2" 1!=2 字元串操作符 前面的例子已經使用了很多次此操作符, 字元串的連接配接可以通過+或+=實作, 其他類型的與字元串進行+操作時會先轉化為字元串 "str ? " + true str ? true
3. 優先級與結合性
優先級 | 類型 | 結合性 | ||
---|---|---|---|---|
1 | () | 括号操作符 | 由左至右 | |
[] | 方括号操作符 | |||
!, +(正号), -(負号) | 一進制操作符 | 由右至左 | ||
自增自減操作符 | ||||
3 | *, /, % | |||
4 | +. - | |||
<<, >> | ||||
6 | >, >=, <, <= | |||
==, != | ||||
9 | ||||
11 | && | |||
12 | ||||
13 | ?: | 條件操作符 | ||
14 | = |
流程控制
程式在執行時會出現各種情況, 例如之前通過關系操作符和邏輯操作符得出的結果, 是否應該走向不同的程式分支, 至于分支的實作就屬于流程控制. 另外, 程式可能會出現不斷執行某語句知道某些條件不成立為止的情況, 這也屬于流程控制. Java處理流程控制的關鍵詞和語句包含if-else, while, do-while, for, return, break, continue, switch
1. if-else
if-else語句主要時依據if語句的判斷結果, 選擇不同的分支路徑. 此語句有一些其他的寫法, 比如if不帶else, 或者else後面可以再連接配接一個if語句繼續進行條件判斷
if (num < 10) {
"num < 10".sout
}
if(num < 100) {
"num < 100".sout
} else {
"num >= 100".sout
}
if(num < 50) {
"num < 50".sout
} else if (num < 100) {
"num >= 50 and num < 100".sout
} else {
"num > 100".sout
}
2. switch
當使用if-else語句時. 如果判斷的條件過多, 可能會出現大量的if-else語句, 這樣的代碼可讀性應該時很差的. 這種情況可是選擇使用switch語句, switch給出所有待選條件, 當符合條件判斷時将開始執行
switch(num){
case 1: "num is 1".sout break;
case 2: "num is 2".sout break;
case 3: "num is 3".sout break;
default: break;
}
switch主要寫法如上所示, 如果去掉case後面的break語句, 那麼将會一直向下執行直到執行break或結束. 連續執行的特性在實際使用時會有用處, 但是在沒有徹底搞清楚前不建議使用
3. for
for循環需要依靠三個字段(初始值, 結束條件, 遊标移動)來達成循環的目的, 但是也有(類型 循環變量:變量)的形式
int[] arr = new int[10];
for(int i = 0; i < 10; ++ i) {
arr[i] = i;
}
for(int i : arr){
i.sout
}
4. while/do-while
while也是一個循環控制的方法, while後面跟随一個判斷條件, 當條件成立時則立即執行後面程式段的語句, do-while則是先執行程式段的語句再進行判斷
int[] arr = new int[10];
int i = 0;
while(i < arr.length){
arr[i] = i;
i ++;
}
int j = 0;
do {
arr[j].sout
j ++;
} while(j < arr.length)
5. break與continue
break與continue在循環中起到非常重要的作用. break可以直接退出整個循環, 當多層嵌套時, 僅退出break所在的循環, continue可以結束本次循環, 當多層嵌套時, 僅結束continue所在的循環
int[] arr = new int[10];
for(int i = 0; i < 10; ++ i) {
arr[i] = i;
}
for(int i : arr){
if(i == 3) {
continue;
}
if(i == 6) {
break;
}
i.sout
}
6. return
return可以直接退出目前的方法, 并且可以帶出傳回值(除void)
如果一個void方法沒有寫return , 那麼可以了解為該方法的最後有一個隐式的return
return後面的語句一般不會執行, 但是有一個例外(finally), 在下面的異常講解時會進行講解
對象
java是一種面向對象的語言, 什麼是面向對象以及如何使用對象?
1. 什麼是對象
面向對象的核心就是把任何事物抽象為類, 這個事物具備的能力就是抽象出來的方法, 這個事物具備的各個實際物品就是抽象出來的字段. 比如手機, 把手機比喻為對象, 那麼手機的硬體(比如CPU, 顯示屏)就是對象裡的字段, 打電話, 上網等就是抽象出來的方法.以學生為例:
public class Student { private int age; /* 學生的年齡 */ private String name; /* 學生的姓名 */ public int getAge() { return age; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public void setName(String Name) { this.name = name; }}/** 建立實體: Student student = new Student();*/
2. 什麼是方法
方法主要包含四個内容, 分别是: 傳回值, 方法名, 參數, 方法體. 同時, 也可使用其他關鍵字來修飾一個方法以達到其他能力.
普通方法的調用格式是Object.fun(args); 比如:
student.getAge()
3. this與static
上面的例子中出現了
this.age = age
, 這裡this的作用是代指調用這個方法的執行個體, 普通成員方法都是預設有this的, 其意義一般是指代調用的執行個體;
static修飾的方法為是靜态方法, 靜态方法無法使用this指代調用, 它隻對所屬的類負責.
4. 通路權限
權限名稱 | 關鍵詞 | 權限範圍 | 用法 |
---|---|---|---|
公開通路權限 | public | 所有都可通路 | 一般用于希望别人使用的方法或公開的api |
保護通路權限 | protected | 派生子類可用 | 不希望所有人都可以使用, 但是希望此方法子類可以使用或更改 |
包通路權限 | (default) | 同一包内可以通路 | 僅希望同一個包内其他的類可以使用它 |
私有通路權限 | private | 僅自己類内部可以使用 | 完全私有方法, 包含類的對象都不可以調用, 一般用于實作類的私有能力, 絕不對外開放 |
5. 垃圾回收
前面用大量的篇幅講解了如何建立一個執行個體以及如何使用這個執行個體。但是這些執行個體使用完 之後去了哪裡呢?如果學過C++, 就知道如果C++建立完對象後置之不理,整個程式肯定會崩潰。但是這個問題對于 Java 來講就沒有那麼重要了, Java 有一套自動回收的機制用于處理建立出來的執行個體.
繼承與多态
繼承是指派生類繼承基類的屬性和某些行為,多态是指派生類在基類的基礎上進行重寫進而表現出來的不同性狀
1. Object
觀察下面代碼的輸出, 了解類和對象的其他特性:
public class Person { private long id; private String name; public Person(long id, String name) { this.id = id; this.name = name; } public static void main(String[] args) { Person person = new Person(20, "Sunhr"); System.out.println(person.toString()); // 輸出為:Person@1b6d3586 }}
在這個例子中, 建立了一個person對象, 然後調用了toString()方法, 代碼很簡單, 問題是, 這個方法從哪來的呢?
在Java中, 所有的類都繼承自Object類. 也就是說除了基本類型, 其他類都是一種Object, 而Object中就可以找到toString()方法, person也就是通過類的繼承得到了這個方法.類似的還用equals方法等等
2. 組合
在了解繼承之前, 先弄清除什麼是組合. 手機相對于物質來講, 屬于繼承各系, 它繼承了物質這個大概念下的一些屬性; 手機對于cpu來講, 屬于包含關系, 這種包含叫做組合.
對剛才的Person做出修改:
public class Person { private long id; private String name; private Eyes eyes = new Eyes(); public static class Eyes { public String left = "左眼"; public String right = "右眼"; }}
這裡的Person包含了一個靜态内部類的執行個體. Person是對一個事物的抽象, 但是這個事物是有很多部分組成的, 每個部分也可以抽象出來, 最後在Person中組合在一起.
3. 繼承
繼承是在同一種共性基礎上的細分和豐富, 在基類中定義此類事物的共性, 在派生類中對基類中定義的共性進行具體的實作或者修改, 并且添加自己的特性.
public class Animal { public int weight; public Animal(int weight) { this.weight = weight; } public void move() { "Animal can move".sout }}public class Cat extends Animal { public String roar = "ao"; public Cat(int weight, String roar) { super(weight); this.roar = roar; } @Override public void move() { "Cat can move".sout }}
這個例子中先定義了基類Animal, 在基類中定義了動物的共有屬性weight和方法move(); 派生類Cat中通過extends聲明繼承Animal, 添加了自己的屬性roar, 并且重寫了move方法.
引入了繼承後, 一個對象的構造順序又變得更加複雜了, 當建立一個派生類對象的時候, 原則是先構造此派生類的基類部分, 再構造派生類的新定義部分.
4. 多态
動态綁定: 執行時判斷所作用對象的實際類型
多态的實作基于動态綁定, 是指用基類的引用指向派生類的執行個體, 當調用方法時再确定應該調用基類的方法還是派生類的方法.基于上面的例子, 再次引入一個Fish:
public class Fish extends Animal { public String livein = "water"; public Fish(int weight, String livein) { super(weight); this.livein = livein; } @Override public void move() { "Fish can swim!".sout }}
下面時分别建立Animal, Cat, Fish并引用這三個執行個體進行操作的例子:
public static void main(String[] args) { Animal animal = new Animal(10); Cat cat = new Cat(10, "ao!"); Fish fish = new Fish(10, "water"); Animal animals = new Animal[3]; animals[0] = animal; animals[1] = cat; animals[2] = fish; for(Animal tmp: animals) { tmp.move(); }}
這段代碼的for循環中, 都是用Animal引用指代數組中執行個體, 但是調用move方法時卻有不同的表現, 這就是多态. 多态是用基類指代派生類, 在實際調用時調用派生類的實作. 通過基類的引用可以引用基類中定義的字段, 例如weight, 但是無法使用派生類中添加的字段.
5. 接口
設想, 把上面的一堆Cat類的執行個體放到一個容器中(比如List), 希望按照每個執行個體的重量從小到大排序, 應該怎麼辦? 解決的方法就是接口, Cat可以通過實作Comparable中的compareTo()方法使得允許Cat類定義執行個體間的比較方式.
6. 抽象類
抽象類就是不可建立執行個體的基類. 比如之前的Animal可以做出如下修改:
public abstract class Animal { public int weight; public Animal(int weight) { this.weight = weight; } public abstract void move();}
容器
容器是存放對象的地方, 當大量的對象需要在記憶體中存在, 并且單個對象分别使用很不友善的時候, 就是容器的應用場景. Java存放資料的方式有很多種, 例如固定大小的資料以及可以自動調整大小的容器類. 而容器類經過多個版本的疊代, 繼承關系較為複雜, 目前比較常用的有List, Set, Map
1. 數組
數組對于容器類, 效率更高, 但是在生命周期内不可改變數組大小, 數組有length字段, 用于通路數組的大小. "[]"的文法可以通路數組成員, 數組有多元的能力, 可以建立二維或以上的數組, 下面将示範一維數組與二維數組的建立
String[] arr1 = new String[5];String[][] arr2 = new String[2][];arr2[0] = new String[2];arr2[1] = new String[4];
2. List
容器List是一個清單, 但是Java對清單的實作有兩種, 一種是類似數組的實作(ArrayList), 一種是類似連結清單的實作(LinkedList), 這兩種List都可以通過List類進行引用并調用方法. ArrayList在插入方面不如LinkedList, 但是LinkedList在擷取清單中的值方面不如ArrayList. 實際使用時可以根據情況選擇. 下面将示範List的建立與增删查改等
List<String> list = new ArrayList<String>();// List<String> list = new LinkedList<String>();list.add("one");list.add("two");list.add("three");list.get(2);list.remove("three");list.contains("one");list.set(1, "2th");list.indexOf("2th");
3. Set
Set是一個集合, 它不保證存取的順序, 它的主要特征是儲存值的唯一性, 判斷儲存的對象是否相等, 可以使用equals和hashCode方法. Set同樣分為兩種: HashSet和TreeSet
4. Map
Map通過鍵值對儲存, 可以通過鍵來擷取值. HashMap是最常用的Map. 其通過散列的形式以達到快速存儲和空間控制的目的.
泛型
泛型最常見的使用場景是在容器内, 容器提供了儲存對象的通用能力, 其他所有類型的對象都可以放入容器之内.
1. 泛型的基本使用
class A { public void print() { "I'm A".sout } }class B extends A { public void print() { "I'm B".sout } }class C extends A { public void print() { "I'm C".sout } }public class TempleteTypeErase { public static <T> void print(List<T> list) { for(T t : list) { if( t instanceof A ) { "I'm A".sout } else if( t instanceof B ) { "I'm B".sout } else if( t instanceof C ) { "I'm C".sout } else { "I'm ?".sout } } } public static void main(String[] args) { List<A> as = new ArrayList<A>(); as.add(new A()); as.add(new B()); as.add(new C()); for(A a : as) { a.print(); } print(as); /* * I'm A * I'm B * I'm C * I'm A * I'm B * I'm C */ }}
2. 通配符
上面例子中, A是B的基類, 但是
List<A>
與
List<B>
之間沒有任何關系! 通配符主要用于參數判斷, 例如某個參數需要A的子類的容器, 通配符隻允許擷取資料, 即無法通過
List<? extends A>
這種向容器添加資料. 原因是這個容器指代了一切繼承自基類的容器, 無法确定是否正确的向容器中添加了适當類型.
public static void testExtend() { List<B> bs = new ArrayLisy<>(); bs.add(new B()); List<? extends A> as = bs; as.get(0).print();}public static void testSuper() { List<A> as = new ArrayLisy<>(); as.add(new A()); List<? super B> bs = as; bs.get(0).print();}
3. 泛型接口
Java的泛型差別于C++的泛型主要在類型擦除. 就是說Java泛型隻在編譯期間進行靜态類型檢查, 編譯器生成的代碼會擦除相應的類型資訊, 這樣JVM根本不知道泛型所代表的具體類型. 可以定義一個泛型接口, 然後讓泛型類聲明傳進的具體類型必須實作此接口, 這樣還保留部分接口能力. 常用的泛型接口時
Comparable<T>
, 可以保留對象比較的能力.
/* 一個泛型接口 */public interface PrintInterface<T> { public void print(); /* 保留了列印能力 */}
4. 自定義泛型
class Tomato implements PrintInterface<Tomato> { @Override public void print(){ "It's Tomato!".sout }}public class CustomTemplete<T extends PrintInterface<T> > { public T data; public void print() { data.print(); } public static void main(String[] args) { CustomTemplete<Tomato> customTemplete = new CustomTemplete<>(); customTemplete.data = new Tomato(); customTemplete.print(); }}
異常
對于使用java編寫的程式, 編譯器在編譯的時候會進行文法檢查等工作. 但是有一些程式中存在的問題在編譯階段無法識别的, 例如用Java實作一個電腦, 當使用者輸入1/0的時候, 你應該怎麼辦? 這就是異常處理存在的原因. 這些錯誤會導緻程式無法繼續運作, 而異常處理就是處理這些錯誤的.
1. 運作時異常
運作時異常時程式在執行過程中出現錯誤調用而抛出的異常, 這種異常可以在編寫時避免.
public class JavaRuntimeException { public static void testDivisor() { int i = 1/0; } public static void main(String[] args) { testDivisor(); }}
運作結果如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero at JavaRuntimeException.testDivisor(JavaRuntimeException.java:3) at JavaRuntimeException.main(JavaRuntimeException.java:6)
用一個整數除以0在算術上是明顯錯誤的, 但是這個問題編譯器目前是不會報錯的, 隻會在執行時抛出一個異常, 異常包含錯誤的類型和代碼的位置, 可以找到問題的所在并進行優化, 下面用異常捕獲來處理這段代碼:
public static void testDivisor() { try { int i = 1 / 0; System.out.println("i = " + i); } catch (Exception e) { System.out.println("divisor can not be zero"); }}/** divisor can not be zero*/
這裡使用了try/catch進行了代碼運作和異常捕獲, 當然, 這裡隻作為示範, 實際項目中一般先用if進行判斷, 而不用異常捕獲來進行處理. 下面的代碼示範了空引用異常:
public static void testNullPoint(){ Person person = null; System.out.println(person.id);}/*Exception in thread "main" java.lang.NullPointerException at JavaRuntimeException.testNullPoint(JavaRuntimeException.java:12) at JavaRuntimeException.main(JavaRuntimeException.java:15)*/
這種空異常的避免方法一般也是在調用前對不确定是否初始化的對象進行非空判斷, 進而避免此異常.
下面為常見的List異常.
public static void testArrayRemove() { List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); int idx = 0; for (String string : list) { System.out.println(string); idx ++; if(idx == 1) { list.remove(idx); } }}/*oneException in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at JavaRuntimeException.testArrayRemove(JavaRuntimeException.java:23) at JavaRuntimeException.main(JavaRuntimeException.java:32)*/
Java的運作時異常有很多種, 例如數組越界異常, 類型轉換異常等等. 異常的處理不是背出來的, 在實際的代碼種去解決異常才是學習方法.
2. 檢查性異常
運作時異常基本都可以避免, 隻要代碼足夠嚴謹就不會出現運作時異常. 是以真正代碼中要處理的時檢查性異常. 這就涉及異常的抛出和捕獲, 在抛出異常的地方使用throw關鍵字抛出, 在抛出異常的方法後添加
throws 異常類名
. 異常捕獲的地方使用
try-catch-finally
. 這裡以讀取檔案作為例子:
public static String readFile() { boolean bool = true; StringBuilder builder = new StringBuilder(); try { FileReader fileReader = new FileReader("src/test.txt"); char[] cs = new char[10]; while (fileReader.read(cs) != -1) { builder.append(cs); cs = new char[10]; } } catch (Exception e) { bool = false; e.printStackTrace(); } finally { if(bool) { System.out.println("read file ok!"); } else { System.out.println("read file fail!"); builder.replace(0, builder.length(), "fail"); } } return builder.toString();}/*read file ok!12345678900987654321end */
在這個例子中, 使用檔案讀寫類 FileReader讀取一個檔案, 在建立這個類的時候, 編譯器強制要求把這個方法放入一段try語句中, 或者這個方法向外抛出異常(添加throws)由外層進行處理, 否則編譯不通過; 這是差別于運作時異常的地方, 運作時異常運作編譯通過.
3. 自定義異常
在實際程式設計中, 可能希望抛出一個已有的異常類型無法準确表達的異常, 例如資料庫中的資料出現了業務邏輯上的錯誤. 這時候就需要自己定義一個異常, 自己定義異常隻需要繼承相關的異常類就可以.
public class CustomRuntimeException extends RuntimeException {}public class CustomException extends Exception{ }public class CustomExceptionDemo { public static void testRuntimeException() { throw new CustomRuntimeException(); } public static void testException() throws CustomException { throw new CustomException(); } public static void main(String[] args) { try { testException(); } catch(CustomException e) { "catch CustomException".sout } catch(Exception e) { "catch Exception".sout } }}/** catch CustomException*/
上面自定義了兩個異常, 但是兩個異常的繼承關系不一樣, 這兩種繼承關系會導緻實際使用中存在差別. 繼承自
RuntimeException
的異常當用throw抛出時, 包含它的方法不需要用throws聲明要抛出異常, 而繼承自Exception則需要. 用catch捕獲異常是按照順序從第一個比對的異常類型進行捕獲的, 一般會把基類Exception放到最後, 防止它攔截了其他的捕獲.
異常還包含其他的一些方法可供使用, 但是對于簡單情況, 隻需要繼承一個異常, 并且用類的名字來區分異常類型就可以了.
I/O
實際的業務中存在大量的互動和通信需求, 這就需要對I/O有相應的了解. 有了I/O才能把程式組成一個龐大的系統, 否則隻能是一個個程式計算的孤島. 雖然很多元件都封裝了簡單易用的I/O操作, 但是了解一些Java的基本I/O還是對學習有幫助的.
1. 控制台I/O
在IDE中負責控制台輸入輸出的就是console視窗, 下面通過兩種不同的I/O流分别讀取此資料, 觀察兩種不同I/O的讀取結果, 代碼如下:
public static void testConsoleStreamIO() { try { char c; InputStream in = System.in; do { c = (char) in.read(); System.out.println(e); } while(c != 'q'); } catch (Exception e) { System.out.println("catch Exception"); }}public static void testConsoleBufferIO() { try { char c; BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); do { c = (char) in.read(); System.out.println(e); } while(c != 'q'); } catch (Exception e) { System.out.println("catch Exception"); }}
第一種方法直接擷取系統的位元組輸入流, 讀取流資料分别顯示到控制台; 第二種方法用字元流封裝了系統輸入流, 然後讀取資料顯示至控制台. 使用這兩種方法, 當讀取全英文時沒有分别. 但是讀取漢字時, 使用字元流可以準确讀取漢字, 位元組流則不能. 是以讀取二進制檔案時(例如音頻, 圖檔等), 使用位元組流較為合适, 當讀取漢字時, 使用字元流是合适的方式.
2. 檢視檔案清單
File類是Java對目錄和檔案進行操作的類, 可以用它對檔案進行建立, 改名, 删除等操作. 下面以周遊目錄為例, 簡單介紹File類的使用.
import java.io.File;import java.util.ArrayList;import java.util.List;public class FileListDemo { public static List<String> getFileByDir(File dir) { List<String> list = new ArrayList<>(); for(File item: dir.listFiles()) { if(item.isDirectory()) { list.addAll(getFileByDir(item)); continue; } list.add(item.getName()); } return list; } public static List<String> getFileList(String uri) { List<String> list = new ArrayList<>(); File file = new File(uri); if(file.isDirectory()) { list.addAll(getFileByDir(file)); } else if (file.exists() && file.isFile()) { list.add(uri); } else if (!file.exists()) { System.out.println("file not found"); } return list; } public static void main(String[] args) { System.out.println(getFileList("../")); }}
3. 檔案I/O
檔案的讀寫在檢查性異常已經涉及, 此處不再做示範
4. 序列化
當把一個Java對象存入檔案或者進行網絡通信時, 需要把一個對象轉為一串資料, 并可以再反轉成一個對象, 這就是序列化的需求. Java的序列化用幾種方式: 對象的類繼承Serializable接口, 或者對象的類繼承Externalizable接口, 實作兩個接口的方法; 或者轉換為其他通用格式, 例如Json
- Serializable實作序列化
此例中, Address繼承了Serializable接口, 并且對person标志了transient表示無需序列化.import java.io.*;public class JavaSerialize { static public class Address implements Serializable { public double longitude; public double latiude; public String name; public transient Person person; } public static void write(String uri, Address address) { try { File file = new File(uri); FileOutputStream outputStream = new FileOutputStream(file); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(address); objectOutputStream.close(); outputStream.close(); } catch (Exception e) { e.printStackTrace(); } } public static Address Read(String uri){ Address address = null; try { File file = new File(uri); FileInputStream inputStream = new FileInputStream(file); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); address = (Address) objectInputStream.readObject(); objectInputStream.close(); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } return address; } public static void main(String[] args) { ... }}
-
Externalizable實作序列化
此接口包含兩個方法,
和readExternal
, 可以通過這兩個方法完成序列化的定制.writeExternal
...static public class Address implements Externalizable { public double longitude; public double latitude; public String name; public transient Person person; @Override public void readExternal(ObjectInput arg0) throws IOException, ClassNotFoundException { longitude = arg0.readDouble(); latitude = arg0.readDouble(); name = (String) arg0.readObject(); } @Override public void writeExternal(ObjectOutput arg0) throws IOException { arg0.writeDouble(longitude); arg0.writeDouble(latitude); arg0.writeObject(name); }}...
-
Json
Json時一種輕量級的資料交換格式, 可以把對象序列化為Json格式
5. 網絡I/O(待補)
Java服務之間可以通過網絡進行通信, 進而實作程式間資料的互通, 網絡I/O是Java服務進行微服務化的基礎. 網絡通信一般較為複雜, 一般都由使用的服務架構解決. 是以我tm也不會, 待補吧
并發
一個Java程式運作在一個程序中, 但是, 有的時候你希望一個程式可以同時做好多事情, 比如監聽端口的同時接受資料并進行邏輯計算, 隻有一個運算單元明顯不夠了, 是以這時需要啟動好多個運算單元, 這就是所謂的多線程. 由于多線程本身是一個比較難的知識點, 在此主要是介紹多線程的寫法和一些重點
1. 多線程實作
- Runnable任務
ThreadRunnable 個繼承自 Runnable 的類,如果在主線程中建立這個類,并且調用run 方法,其實它并沒有什麼特殊,隻是正常執行求和的邏輯. Runnable 對多線程的作用就是可以把它傳入 Thread 中,作為建立線程的執行任務,這樣就實作了多線程.public class ThreadRunnable implements Runnable { private int start; private int end; public ThreadRunnable(int start, int end) { this.start = start; this.end = end; } @Override public void run() { int sum = 0; for (int i = start; i <= end; ++ i) { sum += i; } "thread is "+Thread.currentThread().getName()+" start = "+start+"end ="+end+"sum = "+sum.sout } public static void main(String[] args) { ThreadRunnable runnable = new ThreadRunnable(100, 1000); runnable.run(); Thread thread = new Thread(new ThreadRunnable(200, 2000)); thread.start(); /* * thread is main start = 100 end = 1000 sum = 495550 * thread is Thread-0 start = 200 end = 2000 sum = 1981100 */ }}
- 自定義Thread
可以讓一個類繼承自Thread類, 重寫run方法來實作線程的任務單元, 這樣就可以不用把任務單元傳給Thread, 隻要建立此線程并調用start即可實作多線程public class CustorThread extends Thread { @Override public void run() { try { Thread.sleep(1000); } catch(Exception e) { e.printStackTrace(); } this.sout } public static void main(String[] args) { CustorThread thread1 = new CustorThread(); thread1.setPriority(Thread.MAX_PRIORITY); CustorThread thread2 = new CustorThread(); thread2.setPriority(Thread.MAX_PRIORITY); thread1.start(); thread2.start(); /* * Thread[Thread-1,5,main] * Thread[Thread-0,1,main] */ }}
-
線程池
以上兩種都需要手動建立線程, 可以使用線程池進行托管, 省去麻煩的同時還可以複用
ExecutorService eService = Executors.newCachedThreadPool();// ExecutorService eService = Executors.newFixedThreadPool();for(int i = 0; i < 10; ++ i) { eService.execute(new ThreadRunnable(i*100, i*1000));}eService.shutdown();
2. 線程沖突
上面那幾個例子建立的都是獨立的任務單元, 但是如果多個線程中的任務單元是相同的, 且使用了同一份資料, 那麼會發生一些問題.
public class ThreadConflict { private int sum; public int getSum(int start, int end) { sum = 0; for(int i = start; i < end; ++ i) { sum += i; } return sum; } public static void main(String[] args) { ThreadConflict threadConflict = new ThreadConflict(); "main thread sum = " + threadConflict.getSum(0, 1000).sout; ExecutorService eService = Executors.newCachedThreadPool(); for(int i = 0; i < 10; ++ i) { eService.execute(new Runnable() { @Override public void run() { Thread.currentThread().getName() + " sum = " + threadConflict.getSum(0, 1000).sout } }) } eService.shutdown(); }}/*main thread sum = 499500pool-1-thread-1 sum = 499500pool-1-thread-2 sum = 499500pool-1-thread-3 sum = 499500pool-1-thread-1 sum = 499500pool-1-thread-4 sum = 499500pool-1-thread-2 sum = 499500pool-1-thread-6 sum = 499500pool-1-thread-7 sum = 499500pool-1-thread-5 sum = 327769pool-1-thread-8 sum = 743833*/
Java的多線程是搶占式的, 當多個線程搶占同一資源時, 可能線程A運算到一半時B搶占了A重新開始計算, 等到A回去時資源資料已經不是它離開時的資料了. 這種情況下可以使用鎖解決并發導緻的資源搶占問題
3. 鎖
-
Synchronized關鍵字
隻需要在
前加上關鍵字變為getSum
, 就不會出現上面的情況了. 該關鍵字把這個方法設定為同步方法, 當有多個線程希望使用此方法時, 此關鍵字隻允許一個線程獨享此方法, 其他線程處于等待狀态public synchronized int getSum
-
Lock
可以在這個對象中建立一個ReentrantLock執行個體, 對需要加鎖的代碼段前面調用lock方法上鎖, 執行完畢調用unlock解鎖. (此執行個體包含其他幾種加鎖方式, 例如tryLock方法)
private Lock lock = new ReentrantLock();public int getSumByLock(int start, int end) { lock.lock(); try { ... } finally { lock.unlock(); }}public int getSumByTryLock(int start, int end) { try { if(lock.trylock(1, TimeUnit.SECONDS)){ ... } } catch(InterruptedException e){ e.printStackTrace(); return -1; } finally { lock.unlock(); }}
- 讀寫鎖: ReentrantReadWriteLock
反射與注解
對于一個語言來講, 前面的内容仿佛已經足夠全面了, 那麼Java的反射和注解為Java做出了什麼貢獻?
Java的反射機制是指在運作狀态中, 對于任意一個類, 都可以知道這個類的所有屬性和方法; 對于任意一個對象, 都能夠調用他的任意方法和屬性. 這種動态擷取資訊以及動态調用對象的功能稱為Java的反射機制.
注解可以了解為Java對類, 字段或方法的補充說明, Java通過反射讀到注解, 通過注解的說明對被注解的内容進行相應的操作.
Java的反射與注解的意義在于與架構結合, 各個架構正是應用了Java 的反射與注解才能對業務代碼進行加載和整合.
1. 反射
如果沒有特殊需求, 一般的業務邏輯中不會帶有反射, 了解反射的用處和能力即可, 如果有更高的需求, 那麼反射還是要仔細研究的.
2. 注解
通過注解可以更加了解代碼, 并可通過注解解析和使用友善管理代碼
-
Java内置三種注解:
@Override: 目前方法覆寫基類方法
@Deprecated: 表示棄用的方法
@SuppressWarnings: 關閉警告的注解
-
元注解
@Target: 表示注解的可用範圍, 包含CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE. PARAMETER, TYPE
@Retention: 表示注解的應用級别, 分為:SOURCE. CLASS, RUNTIME
@Documented: 可以被Javadoc文檔化
@Inherited: 注解類型被自動繼承
-
自定義注解
自定義注解和接口非常相似, 比如:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface MethodUrl { public int ID() default -1; public String Describe(); public String URL();}/** 使用: @MethodUrl(ID=1, Describe="擷取名字",URL="\getName")*/
-
注解解析
比如上面的自定義注解, 這個注解寫完了怎麼用?難度僅僅在文檔上有用嗎?下面給一個類加上自定義注解, 然後再通過一個解析的方法把注解解析出來
上面代碼建立了一個類, 下面的代碼可以擷取到JavaAnnotation類的注解情況并輸出public class JavaAnnotation { public String name; @MethodUrl(ID=1, Describe="擷取名字",URL="\getName") public String getName() { return name; }}
public static void main(String[] args) { try { Class clazz = Class.forName("xxxxxxx.JavaAnnotation"); Method[] methods = clazz.getMethods(); for ( Method method : methods) { MethodUrl methodUrl = method.getDeclaredAnnotation(MethodUrl.class); if(methodUrl != null) { method.getName() + " function ID is " + methodUrl.ID() + " and url is localhost" + methodUrl.URL() + " for " + methodUrl.Describe().sout } } } catch (Exception e) { e.printStackTrace(); }}
JUnit
當編寫完代碼, 需要對自己寫的功能進行測試時, 可以直接寫一個main來測試自己的代碼, 也可以使用JUnit進行單元測試. JUnit可以保證程式穩定性的同時減少花費在排錯上的時間
1. JUnit的內建
如果非Maven管理的項目有如下兩種選擇:
對于Maven管理的項目, 可以使用如下代碼:
<!-- JUnit test --><dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope></dependency>
2. JUnit的基本使用
- 建立一個測試類
- 在測試類中添加一個方法
- 為方法添加注解@Test
- 執行JUnit的assertEquals來檢查測試是否通過
3. JUnit常用注解
注解名稱 | 含義 |
---|---|
@Before | 運作前調用, 一般用于初始化方法 |
@After | 運作後調用, 一般用于釋放資源 |
@Test | 測試方法, 可以測試方法的執行情況 |
@BeforeClass | 所有用例運作之前隻執行一次, 且方法必須為static void |
@AfterClass | 所有用例運作之後隻執行一次, 且方法必須為static void |
@Ignore | 忽略的測試方法 |