簡介
讀源碼時,或者看同事寫的代碼,經常看到一些不太常見的語法,這里做一個總結
不太常見的語法
成員變量的默認值
案例:
public class Person2 {private String name = "張三";private Integer age;public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}@Overridepublic String toString() {return "Person2{" +"name='" + name + '\'' +", age=" + age +'}';}
}
實例化Person2,會發現,name已經被賦默認值了:
Person2 person2 = new Person2();
System.out.println("person2 = " + person2); // person2 = Person2{name='張三', age=null}
普通的業務代碼中不推薦這么寫,建議把賦值語句寫在構造方法中,語義更明確。
原始類型數據不能和null進行運算
案例:
int i = null;
這一行代碼會報錯,基礎數據類型不是一個對象,不支持賦null值。
我在開發中遇到的一個實際案例,當時沒有注意到這一點,這里我使用一段簡單的代碼來模擬一下:
public class Test3 {@Testpublic void test1() {Integer i = null;if (Color.RED.getId() == i) { // 這行代碼會報空指針異常,可以使用Objects.equals來避免System.out.println("aaa");} else {System.out.println("bbb");}}enum Color {RED(1, "紅色"),BLACO(2, "黑色"),YELLOE(3, "黃色");private final int id;private final String name;Color(int id, String name) {this.id = id;this.name = name;}public int getId() {return id;}public String getName() {return name;}}
}
分析一下原因,因為基礎數據類型不支持和null進行運算,而案例中Integer類型的變量實際上是一個null值,所以會報空指針異常,我當時遇到這個問題,怎么都想不到原因,結果發現,同事定義的枚舉值中,居然使用了int類型,所以最好使用包裝類來作為成員變量的類型,這個案例中,如果id的類型是Integer,這么比較不會有問題。
do while語句
do while循環,第一次時一定會進入循環,在某些情況下使用它更合適,例如,有一種場景,需要循環讀取外部數據并處理,直到讀完,第一次的時候肯定要讀取外部數據,所以這種情況下使用do while會比較合適。
案例:
數據準備: 模擬讀取外部數據,分頁讀取
private static final List<User> list = Lists.newArrayList(new User(1L, "aaa", "aaa@qq.com", 20),new User(2L, "bbb", "bbb@qq.com", 20),new User(3L, "ccc", "ccc@qq.com", 20));private PageBO<User> readList(int pageNo, int pageSize) {PageBO<User> bo = new PageBO<>();bo.setPageNo((long) pageNo);bo.setPageSize(pageSize);bo.setTotalCount((long) list.size());int offset = (pageNo - 1) * pageSize;int limit = Math.min(offset + pageSize, list.size());if (offset < list.size()) {List<User> resultList = new ArrayList<>(list.subList(offset, limit));bo.setRecordList(deepCopyByIO(resultList));}return bo;
}// 使用IO流實現深拷貝
@SuppressWarnings("unchecked")
public static <T> T deepCopyByIO(T object) {if (object == null) {return null;}// ByteArrayOutputStream、ByteArrayInputStream,是內存流,即使不關閉也不會造成資源泄露,例如文件句柄未// 釋放等問題,即使如此,隨時關閉流依然是一個好習慣,而且關閉輸出流會保證緩沖區的數據被正確的寫出try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {objectOutputStream.writeObject(object);try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {return (T) objectInputStream.readObject();}} catch (Exception e) {e.printStackTrace(); // 打印異常信息return null;}
}@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBO<T> {/*** 總條數*/private Long totalCount;/*** 當前頁碼*/private Long pageNo;/*** 頁面大小*/private Integer pageSize;/*** 本頁數據*/private List<T> recordList;
}@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {private static final long serialVersionUID = 8683452581122892181L;private Long id;private String name;private String email;private Integer age;
}
案例1: 使用while語句來實現循環讀取外部數據
@Test
public void test1() {int pageNo = 1;final int PAGE_SIZE = 1;PageBO<User> pageBO = readList(pageNo, PAGE_SIZE);while (CollectionUtils.isNotEmpty(pageBO.getRecordList())) {// 處理數據System.out.println("處理數據 = " + pageBO.getRecordList());// 循環讀取pageNo++;pageBO = readList(pageNo, PAGE_SIZE);}
}
案例2: 將上面的while循環變成do while循環
@Test
public void doWhileTest() {int pageNo = 1;final int PAGE_SIZE = 1;PageBO<User> pageBO; // while 條件表達式中的變量,必須在do while之外才可以被看到do {pageBO = readList(pageNo, PAGE_SIZE);System.out.println("處理數據 = " + pageBO.getRecordList());pageNo++; // 循環讀取} while (CollectionUtils.isNotEmpty(pageBO.getRecordList()));
}
案例3:案例2中有一個問題,最后一次一定會讀到空數據,所以while不可以直接轉換為do while,代碼要做一點改變
@Test
public void doWhileTest2() {int pageNo = 1;final int PAGE_SIZE = 1;PageBO<User> pageBO;do {pageBO = readList(pageNo, PAGE_SIZE);if (CollectionUtils.isEmpty(pageBO.getRecordList())) {break;}System.out.println("處理數據 = " + pageBO.getRecordList());pageNo++; // 循環讀取} while (true);
}
案例4:一個簡單的案例
public static void main(String[] args) {java.util.Scanner scanner = new java.util.Scanner(System.in);int userInput;do {System.out.print("請輸入一個1到10之間的數字: ");userInput = scanner.nextInt();} while (userInput < 1 || userInput > 10);System.out.println("你輸入了: " + userInput);
}
總結:do while語句適合處理第一次一定會進入循環的情況,但是使用普通的while也可以,只是do wihle在語義上更加合適,不過要注意的是,判斷是否需要繼續循環的條件,和while的不太一樣,它們不可以共用。
平時do while使用的不是很多,更多的使用的是while,遇到類似的情況,可以使用更合適的do while。
問題:a = b
的返回值是什么?
答案:返回b
案例:
int a = 10;
int b = 11;
int c = a = b;
System.out.println("c = " + c); // 11
這是在閱讀源碼時看到的一段代碼:
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);
注意看方法中的參數3,q.next = waiters
,最終是把什么值傳遞給了方法?答案是waiters,但同時,q.next的值也被更新為waiters了
雙括號初始化
一種快捷的初始化集合或其他對象的方式
案例:
List<String> list = new ArrayList<String>() {{add("AA");add("BB");}
};
list被初始化為一個 ArrayList 的匿名子類。大括號中的代碼塊是一個實例初始化塊,它在匿名子類創建時執行。
需要注意的是,雖然雙括號初始化語法在某些情況下很方便,但它也創建了一個匿名的子類,會產生一些額外的內存開銷,因為每個實例都是一個單獨的類。此外,這種語法在某些上下文中可能會導致問題,比如在序列化時。
這種初始化方式會創建一個匿名內部類,相較于普通的初始化方式性能較低,不建議使用,而且序列化等方面,可能會有意想不到的坑
迭代器的使用
關于迭代器的幾個問題:
- 自己編寫的類,怎樣可以被for關鍵字遍歷? 實現Iterable接口,這個接口要求返回Iterator實例,通常需要為自己編寫的類實現一個獨有的Iterator。
- 迭代器的初始狀態:迭代器被創建后,指針指向列表的第一個元素,hasNext的第一次判斷,是判斷列表的第一個元素存不存在,next方法,返回當前指向的元素并且指針后移。這里不是絕對的,取決于迭代器的實現邏輯,但是通常是這么實現的。
案例:模擬ArrayList,寫一個基于數組的集合
public class ArrList<E> implements List<E>, Iterable<E>, Serializable, Cloneable {private static final long serialVersionUID = 1234L;private static final int DEFAULT_CAPACITY = 8;private Object[] arr;private int size;private int capacity;public ArrList() {capacity = DEFAULT_CAPACITY;size = 0;arr = new Object[capacity];}// 省略代碼,這里只關系迭代器的實現@Overridepublic Iterator<E> iterator() {return new Itr();}private void checkIdx(int i) {if (i < 0 || i >= size) {throw new RuntimeException("下標越界:" + i);}}private class Itr implements Iterator<E> {private int idx;private int lastRet = -1; // 上次訪問@Overridepublic boolean hasNext() {return idx < size;}@Overridepublic E next() {checkIdx(idx);lastRet = idx;return get(idx++);}// remove方法,只有在調用next方法之后才可以調用remove方法,它用于移除當前元素,// 因為此時指針已經后移,所以特定使用一個變量來記錄指針的上一個位置。@Overridepublic void remove() {checkIdx(idx);ArrList.this.remove(lastRet); idx = lastRet;lastRet = -1;}}
}
重點查看迭代器的實現邏輯,迭代器初始化時,指向列表中的第一個元素,調用next方法,返回當前元素并且指針后移。
之前只是學習過迭代器的正確使用姿勢,自己嘗試寫一個迭代器時,才注意到它內部的邏輯,所以這里做個簡單總結
總結
這里記錄了一些不太常見的語法,跟個人經驗有關,比如我就不怎么使用do while循環來解決問題,但是有些情況下它確實更合適,還有迭代器的使用,不太清楚它的內部邏輯,剩下的就是某些不太常見的語法,或者容易被忽視的問題點。