Java复习笔记
Java复习笔记
Java-Lesson 0 (Resources)
- Java 8 官方文档: https://docs.oracle.com/javase/8/docs/api/
- Google Java Style Guide: https://google.github.io/styleguide/javaguide.html
Java-Lesson 1 (Basic Grammar)
标识符: 标识类/ 变量/ 常量和方法的名字, 由字母 (A-Z, a-z) / 特殊符号($, _) 和数字 (0-9) 构成, 区分大小写, 名字的第一个字符不能为数字, 标识符不可为Java关键字.
Java数据类型可分为基本数据类型 ( Primitive type )和引用数据类型 ( Reference type ), 数组和对象都是引用数据类型.
Java基本数据类型有
byte: -128~127 (1 byte) 的整数,boolean: 布尔型 (实际占1 byte),char: 占2 byte, (16-bit unicode)Java大数: 对于高精度计算的需求, Java Math库提供了BigInteger 和 BigDecimal 以满足任意长度的整数运算和任意精度的浮点数运算, 有一些常量, 如
BigInteger.ZEROBigInteger.TEN, 但是由于Java不像C++那样支持运算符重载, 所以其运算不是使用 + * , 而是add() multiply() 等.Java也支持下划线分隔整数或浮点数, 以及科学记数法 (E或e).
long hex = 0x7f_e9_b7_aa; float expf = 1.39E-43f;final关键字, 修饰常量. 常量命名常常全部大写 (
final: 最后的, 没有后继者(不被修改/改写, 不能被继承...) )Java允许int类型溢出, 这一点与C十分相似.
java中的最大整数常量:
1
int min = Integer.MAX_VALUE;
类型转换: 自动类型转换 -- 类型提升 强制类型转换
(<type>)vari1
2double doubleNum = 9.9;
int intNum = (int)doubleNum; // 9main() 方法要给外部JVM程序调用, 所以必须为public, 在对象没产生前, main()方法就已被JVM调用, 所以必须为static (类方法/ 静态方法)
1
public static void main(String[] args) // 访问修饰符 关键字 返回类型 方法名(参数)
Java-Lesson 2 (Control Flow, Container)
switch语句中default的使用细节:
- default可以随意与case语句更换位置, 不论其在哪, 都是最后被执行
- default语句如果在所有case后面使用, 则可以不用break语句
- default语句如果在部分case之前, 或在所有case之前, 则建议在其语句中加入break语句. 否则, 执行完default语句后, 会从上往下顺序执行case语句, 直到遇到break语句, 如果一直不遇到break语句, 则执行完default下方所有的case语句.
switch的新特性: switch 表达式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33class DaysInMonth {
public static void main(String[] args) {
Calendar today = Calendar.getInstance();
int month = today.get(Calendar.MONTH);
int year = today.get(Calendar.YEAR);
int daysInMonth = switch (month) {
case Calendar.JANUARY,
Calendar.MARCH,
Calendar.MAY,
Calendar.JULY,
Calendar.AUGUST,
Calendar.OCTOBER,
Calendar.DECEMBER -> 31;
case Calendar.APRIL,
Calendar.JUNE,
Calendar.SEPTEMBER,
Calendar.NOVEMBER -> 30;
case Calendar.FEBRUARY -> {
if (((year % 4 == 0) && !(year % 100 == 0))
|| (year % 400 == 0)) {
yield 29;
} else {
yield 28;
}
}
default -> throw new RuntimeException(
"Calendar in JDK does not work");
};
System.out.println("There are " + daysInMonth + " days in this month.");
}
}注意, 与C的区别, 分支的控制变量也可以是字符串.
变化1, switch 代码块出现在了赋值运算符的右侧. 这也就意味着, 这个 switch 代码块表示的是一个数值, 或者是一个变量. 换句话说, 这个 switch 代码块是一个表达式.
变化2, 是多情景的合并. 也就是说, 一个 case 语句, 可以处理多个情景. 这些情景, 使用逗号分隔开来, 共享一个代码块.
变化3, 无break.
变化4,
->, 箭头标识符, 这个符号使用在 case 语句里, “case L ->”. L就是要匹配的一个或者多个情景. 替代的是传统冒号标识符:, 但我们依然可以在 switch 表达式里使用冒号标识符, 使用冒号标识符的一个 case 语句只能匹配一个情景 (但是极不推荐你用这种形式).变化5, 是箭头标识符的右侧, 可以是表达式/ 代码块或者异常抛出语句, 而不能是其他的形式. 如果只需要一个语句, 这个语句也要以代码块的形式呈现出来. (即必须括上大括号, 否则报错)
1
2
3
4
5case Calendar.JANUARY,
// snipped
Calendar.DECEMBER -> { // CORRECT, enclosed with braces.
yield 31;
}foreach语法
for (char c : array1) { System.out.println(c); }break标签 (, continue标签) : 可以打破任意层循环
1
2
3
4
5
6
7Loop1:
for(i = 0; i < 10; i++) {
Loop2:
while(true) {
break Loop1;
}
}int[][] a = new int[3][4]; //矩阵int[][] a = new int[3][]; //不规则a[0] = new int[3] {1, 1, 4}ArrayList<String> myList = new ArrayList<String>();1
2
3
4
5
6
7
8ArrayList<String> food = new ArrayList<String>();
food.add("pizza"); // 增
food.set(0, "pasta"); // 改
System.out.println(food.get(0)); // 查
food.remove(0); // 删
food.size();
food.clear;排序:
1
2
3import java.util.Collections;
Collections.sort(food, Comparator.reverseOrder()); // 列表排序, 而数组排序用Arrays.arraylist.sort(Comparator c)list.sort(Comparator.reverseOrder()); // 从大到小排序addAll(int index, Collection c)将c中所有元素插入到ArrayList中index的位置. (index缺省时为尾插)removeIf(Predicate<E> filter)如list.removeIf(e -> e.contains("Tao"))clone()复制一份containsindexOf()返回元素的索引subList()toArray()toString()注意, List只能装对象, 不能装原始类型(int/ char/ float)! 所以常常要类型转换为
Integer等类.列表
ArrayList用作动态数组:1
2
3
4
5
6
7List L = new ArrayList(); // 为什么不用List来new一个对象, 因为Java中List是接口, 而ArrayList可以看作是一个实现. List里面的方法, ArrayList都有
L.add("a");
List<Integer> L1 = new ArrayList<Integer>();
L1.add(1);
Collections.sort(L1);ArrayList初始化:1
2List<Integer> numbers = new ArrayList<Integer>(Arrays.asList(100, 200, 300));
List<String> names = Arrays.asList("Alex", "Bob");ArrayList二维1
2ArrayList<ArrayList<String>> groceryList = new ArrayList();
groceryList.add(new ArrayList().add("soda"));拷贝数组: (比for循环拷贝更快)
1
System.arraycopy(src, 2, dst, 0, 7); // src, 偏移量, dst, 偏移量, 复制元素的个数
Arrays类中常用函数
Arrays.sort(array)– 将数组排序 (升序)Arrays.toString()– 将数组变为已读的字符串Arrays.asList()– 将数组转换成列表Arrays.asList()方法返回由指定数组支持的固定大小的列表. 由于无法对数组进行结构修改, 因此无法向列表中添加元素或从中删除元素. 该列表将抛出一个UnsupportedOperationException如果对其执行任何调整大小操作.
List与数组之间相互转换
- 数组 -> List
1
2
3
4int[] b = new int[]{3,8,20,7,11,25};
// int[] -> Integer[]
Integer[] boxB = Arrays.stream(b).boxed().toArray(Integer[]::new);
List<Integer> list = new ArrayList<Integer>(Arrays.asList(boxB));1
2
3Integer[] a = new Integer[]{3,8,20,7,11,25};
List<Integer> list = new ArrayList<>();
Collections.addAll(list, a); // 利用集合工具类- List -> 数组
1
2
3
4
5
6// List转Object[]
Object[] objs = list.toArray();
// Object[] 转Integer[]
Integer[] nums = Arrays.stream(objs).toArray(Integer::new);
// Integer[] 转int[]
int[] arr = Arrays.stream(nums).mapToInt(Integer::valueOf).toArray();1
2
3int[] arr = list.stream().mapToInt(Integer::intValue).toArray;
// or
int[] arr = list.stream().mapToInt(Integer::valueOf).toArray;Java HashMap, 类似Python的字典.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import java.util.Map;
import java.util.HashMap;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("BMW", 10000);
System.out.println(map.get("BMW")); // 10000
}
}
public class MapDemo {
public static void main(String[] args) {
Map<String, String> L = new TreeMap<>(); // 这是一种泛型的写法
L.put("dog", "woof");
L.put("cat", "meow");
String sound = L.get("cat");
}
}
Java-Lesson 3 (File, Exception)
File类 (java.io.File)
创建文件, 可以使用
createNewFile()方法, 此方法会返回一个布尔值: true 代表此文件成功创建; false 代表此文件已存在. 要注意此方法要放入 try … catch 中, 防止 IOException 异常的出现.1
2
3
4
5
6
7
8
9
10
11
12
13import java.io.File;
import java.io.IOException;
public class FileHandler{
public static void main(String[] args) {
File newFile = new File("newFile.txt");
try {
newFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}File类的内置方法:
getName()getAbsolutePath()canWrite()canRead()length()删除 (空) 文件夹:
.delete()Java函数 (方法)
1
2
3
4
5
6
7
8
9
10
11
12public class Main {
public static void main(String[] args) {
System.out.println(Fact(3));
}
static int Fact(int n) {
if (n = 0) {
return 1;
}
return n * Fact(n - 1)
}
}异常 (Exception) :
1
2
3
4
5
6
7
8
9try {
System.out.println(0 / 1);
int []array = new int[]{1, 2};
System.out.println(array[3]);
} catch (Exception e) {
System.out.println(e);
} finally {
System.out.println("continue...");
}关键字
throwsthrow1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Main {
public static void main(String[] args) {
try {
badCode();
} catch (Exception e) {
System.out.println(e);
}
}
public static void badCode() throws ArithmeticException, IndexOutOfBoundsException {
System.out.println(0 / 0);
int[] array = new int[]{1, 2};
System.out.println(array[3]);
}
}throws关键字要写在会出现异常的方法后面 (称为"异常说明"), 并定义好会出现的异常类型, 如有多个异常, 则用逗号隔开. 这样我们就可以将处理异常这种麻烦事交给上层处理. (虽然异常说明跟在出异常的方法声明的后面, 但是它并不是方法签名或方法类型的一部分. )
我们可以使用throw关键字来抛出一个异常:
1
2
3
4
5
6
7
8
9
10
11public class Main {
public static void main(String[] args) {
checkAge(17);
}
public static void checkAge(int age) {
if (age < 18) {
throw new ArithmeticException("Access denied: You must be at least 18 years old.");
} else {
System.out.println("Access granted: You are old enough!");
}
}可以抛出多个异常, 相应的, 也要处理多个异常.
检查型异常 (checked exception) : 在编译时被检查并强制实施的异常. 非检查型异常 (unchecked exception) : RuntimeException, 运行时异常.
子类重写父类方法要抛出与父类一致的异常, 或者不抛出异常 (在继承和重写中, 异常说明只缩不扩); 子类重写父类方法所抛出的异常不能超过父类本身的范畴. (如果子类多一个Runtime Exception, 那么编译器也不会报错. 因为运行时异常与编译无关, 如
NullPointerException)但是构造方法是个例外, 子类的构造器可以无视基类构造器随便抛出任何异常. 但是, 如果子类的构造器调用了基类的构造器或者被编译器自动加入了一个无参的基类构造器, 那么子类构造器必须抛出所有基类构造器抛出的异常. 而子类构造器不能捕获基类构造器所抛出的异常.
如果我们要创建一个自定义的异常, 只要继承
Exception基类即可:1
2
3
4
5
6class MyException extends Exception {
MyException();
MyException(String msg) {
super(msg);
}
}对知道怎么解决的异常, 要捕获; 对于自己不知道怎么解决的异常, 要抛出. 例子如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Calculator {
public int div(int a, int b) throws Exception {
if (b == 0) {
throw new Exception("divided by zero");
}
return a / b;
}
public static void main(String[] args) {
try {
System.out.println(new Calculator().div(Integer.parseInt(args[0]),
Integer.parseInt(args[1])));
} catch (Exception e) {
System.out.println("Exception");
}
}
}结合日志使用异常.
异常链: 重新抛出异常时, 希望保留原始异常的信息.
所有的
Throwable子类都可以选择在构造器中接受一个cause对象, 作为原始的异常.1
Throwable(String message, Throwable cause)
针对文件等资源的IO异常问题 (需要嵌套捕获, 不方便), Java 7引入了自动关闭资源 (回收), 在相关资源类 (如
FileInputStream) 中实现了AutoCloseable接口, 里面具有close方法, 会自动关闭打开的资源, 这样一定程度上避免了使用资源的IO时嵌套的复杂的异常捕获代码. (注意以下代码中圆括号的使用) (圆括号里面的内容叫资源说明头, 事实上, 资源说明头可以包含多个定义语句)1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.io.*;
public class TryWithResources {
public static void main(String[] args) {
// 不是直接 {} 而是在 {} 之前用 () 包裹一个准备资源的语句 -- ()里的语句也可能发生异常, 但是这个异常是自动被处理的 (自动关闭), 这样在进入实际的 try 的内部即{}内, 那么这个资源被保证一定正常打开了, 如果()内部异常了, 打开的资源会被自动关上.
try (
InputStream in = new FileInputStream(new File("hi.java")); // 此处(位于圆括号包围内容的结尾)的分号是可选的
// FileInputStream类实现了java.lang.AutoCloseable接口
) {
int contents = in.read();
} catch (IOException e) {
// handle the error
}
}
}对于一段代码可能产生多个异常, 一种做法是逐个捕获, 即
try {} catch (Exception1) {} catch (Exception2) {} ...如果多个异常之间有继承关系, 那么子类捕获在前. ( 按从小到大顺序捕获异常, 先子类后父类. )这样的做法可能不够灵活, 可以使用组合捕获:
1
2
3
4
5try {
x();
} catch (Exception1 | Exception2 | Exception3 e) {
process();
}但是, 由于一次异常只可能是具体的一种, 所以在若干个catch块之中至多只会执行一个.
如果程序规模足够大, 那么使用检查型异常可能不是一个好主意 (详见 On Java 基础卷 第15章) , 有两种补偿的方法 -- 1. 利用链式异常将检查型异常包在
RuntimeException中 2. 创建自己的RuntimeException的子类.
Java-Lesson 4 (String)
String类: final的不可被继承, 本质是final的char数组
String中的内置函数
- length – 长度
- toUpperCase – 转换为大写
- indexOf(substr) – 找到特定字符串的位置
String类中的方法
==(注意, 由于String是不可变类, 以下方法均不能修改字符串本身, 而是返回一个改后的String类对象)==
.equals()- 判断字符串==是否相等 (所以常用equals()方法)==, 而==是判断是否==同一== (这是一个很坑的点, 应该说, 在Java判断相等的时候, 你==只应该想到equals这一种做法!!!==)trim()- 移出前导或尾随的空白符toCharArray()- 转换成字符数组substring(start, stop)[起始下标, 终止下标) 终止下标可省, 表示到末尾 (前闭后开)charAt(index)字符串里面下标为index的字符replace(s1, s2)把字符串中的s1变为s2.split()返回一个String[] 类型, 为分割出的子串的数组..length()返回长度String类对象有个特殊的创建方式,
String x = "abc"x是"abc"对象的地址, 也叫作"abc"对象的引用.String对象可以通过 "+" 串联, 也可通过 concat() 来串联. 但是字符串连接符
+可以把数字 (int/double) 或 字符(char) 和字符串加在一起, 而concat()方法只能连接字符串.也可以先转换成
StringBuilder, 通过append来增加 (StringBuilder还自带了reverse方法), 然后通过.toString()方法回来.从字节数组创建字符串
1
2
3byte[] buffer = new byte[1024];
int len = System.in,read(buffer);
String s = new String(buffer, 0, len);Java常考题 -- 以下代码输出什么:
1
2
3
4
5
6
7
8
9
10
11String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);
// true, 因为这里"abc"存在常量池中, 在代码区, 指向同一块内存
// 这是java的一个特性 (feature) 而判断相等(非同一)最好还是用.equals()方法
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2);
// false, 因为此时"abc"在堆区, 他们已是不同的对象
// PS: str1, str2这样的局部变量在栈区
Java-Lesson 5 (OOP)
类声明
[modifiers] class ClassName [extends SuperClassName] [implements InterfaceNames] {...}
modifier (修饰符)
final: 不可有任何子类public: 可被包外的类访问缺省: 是一个缺省类 (the default 又叫包私有 package-private) 只可被当前包里的类访问.
abstract: 虚类 (抽象类) 只可被继承, 不可被实例化对象包 (package) 的概念
如果我想写一个类叫Student, 但是另一个人也要写一个Student类, 这就会产生名字冲突, 为了解决这个问题, 我们需要 package.
Java定义了一种命名空间, 叫做包 (package). 一个类总是归属于某一个包, 所以一个类的完整名字便是 package_name.class_name.
JDK中的 Arrays 类存放在 java.util 包中, 那么完整类名就是 java.util.Arrays.
位于同一个包的类, 可以访问包作用域的属性和方法. 如果我们想要在其他的包中调用方法, 我们需要使用import关键字.
创建包的时候, 需要为这个包取一个合适的名字. 之后, 如果其他的一个源文件包含了这个包提供的类/ 接口/ 枚举或者注释类型的时候, 都必须将这个包的声明放在这个源文件的开头.
包声明应该在源文件的第一行, 每个源文件只能有一个包声明, 这个文件中的每个类型都应用于它. (package pkg1.pkg2...)
如果一个源文件中没有使用包声明, 那么其中的类/ 函数/ 枚举/ 注释等将被放在一个无名的默认包 (unnamed package) 中.
Maven: 用于调用包
Python目录里必须包含一个
__init__.py文件以便被Python视为包. (这个__init__.py使得Python有类似Java的包结构, 可以为空, 也可以包含一些初始化代码 (被首先执行) )描述 常用类 java.lang 语⾔包 (默认引⼊) Object、String、Math、System、Exception、Class、Thread、Throwale java.io 输⼊输出流的⽂件包 OutputStream、InputStream、PrintWriter、File、FileInputStream、FileOutputStream、BufferedReader、BufferedWriter java.util 实⽤⼯具包 Date、Calendar、List、Map、Set、Stack、Random、Currency、Locale java.net ⽹络包 URL、Socket、ServerSocket、HttpCookie java.sql 数据库处理包 Connection、Statement、PreparedStatement、ResultSet java.text ⽂本处理包 Format、DateFormat、NumberFormat 第三方java库: Junit(用于测试), Weka(机器学习), Hadoop(分布式)
成员变量
[accessSpecifier] [final] type varaibleName [=initial_value];
访问控制
public--所有都能访问 (可修饰类 (仅有public和default可以修饰类))
protected--当前类/当前类所在包/当前类的子类能访问, 其他不可
default (缺省)--当前类/当前类所在包能访问, (不在同一个包的) 子类不可
private--只有当前类能访问 (所以可以设定get和set方法来进行访问和修改).
(protected -- 包可达性 + 子类可达; default -- 包可达性)
举例:
1
2
3
4
5
6package x.y;
public class A extends B {
int i = 1;
}
// 此处变量i仅能被y包下的类访问.
// 因为虽然A是public, 但是i是default, 所以A的子类不能存取i一般来说, 对于"敏感"的数据应该设为私有变量, 从而对外界进行隔离. 通过公共方法 (get和set) 来进行访问和修改这些变量. (Java的封装性)
get... 方法 -> 访问子/ 观察子; set... 变异子
方法一旦被final修饰, 则不可被重写 (覆盖).
javadoc: 文档, 提供了类或者方法的一些说明, 如参数/ 返回值, 这是java注释的⼀种. /** ... */
java方法的参数如果是Primitive type, 则传递过来的必须是单一值, 称Pass by value; 如果是Reference type, 则传递过来的必须是内存地址, 称Pass by reference.
值传递 (Pass by value) 在java中, 方法的实参是通过值传递的, 当方法调用时, 实参的值的拷贝会赋给方法中的参数变量. 方法中本意读该拷贝值做改变, 但不会影响到原来的变量. (方法外和内严格分开, 且无指针, 所以Java写不出swap(a, b)函数.)
Java函数参数传递某个对象的引用, 如果这时函数通过该引用改变了这个对象, 那么外部的索引指向的对象==也被改动了==. (类似C的指针, 不是pure function, 不安全, 所以不推荐).
Integer类型的整数对象, 因为是不可变类 (类属性不会被修改), 所以不会被函数传参后通过对形参的操作改变变量名指向的东西. String类也是不可变类, 所以==函数内部改动无法影响到外部的引用名所指对象==. 所谓的函数内部的改动只是把这个引用的副本指向了另外的东西, 而不是把引用的副本指向的东西修改了! (所以在外部, 那个引用该指向啥还指向啥)
Python中的方法中参数是什么传递? -> 赋值传递, 与Java本质一样, 只是Python没有原始类型. (所以说Java的变量名也更像标签, 用Python理解Java, 这两种语言在 pass by assignment上J几乎一致)
对象数组: 先定义一个存放对象的数组, 再建立每个对象的存储空间.
1
2
3
4
5
6
7
8
9class Book {
String name;
double price;
}
Book[] books = new Book[2]; // 定义一个存放Book对象的数组
/* 建立每个对象的存储空间 */
for (int i = 0; i < books.length; i++)
books[i] = new Book();构造方法 (同名, ==无返回值==): 对象的实例化通过构造方法来实现, new语句时自动调用, 而不能显式地调用.
构造方法名字必须与类名相同.
构造方法无返回值 (不是返回void型, 不能加void修饰, 加了返回值修饰的就成了一般方法==不是构造方法==).
构造方法可以有多个, 构造方法可以==重载== (运行时会根据参数形式来具体选择构造方法).
当没有声明构造函数时, 默认含有一个无参数的构造函数, 当显式声明构造函数后, 默认的构造函数就不存在.
继承:
1
2
3
4
5
6
7
8
9
10
11public class Dog extends Animal {
public Dog(String name) {
super(name); // 由于super就是父类名, 而父类构造方法和类同名, 所以此处子类的构造方法中用super调用了父类的构造方法
}
public void greet() {
System.out.println("WangWang..., I am " + this.name);
}
public void run() {
System.out.println("I am running!");
}
}==静态类型和实际类型==:
1
Base base = new Child(); // Base: 静态类型, Child: 实际类型
一个变量都有两个类型.
静态类型: 引用变量的类型, 在编译期确定, 无法改变
实际类型: 实例对象的类型, 在编译期无法确定, 需在运行期确定, 可以改变
static关键字: 是Java中用来表达隶属于"类"本身, 而不隶属于类的对象的一个关键字. (可修饰变量(静态变量), 方法(静态方法), 语句块(初始化类变量))
类变量 (Class Variable) / 静态变量: 表示类的属性, 为所有该类的对象所共有, 为了与实例变量有所区别, 前面加static. 用来表达所有对象的共有属性, 在java类1加载之后就得到了相应的内存, 其只有一份, 不会随着对象的创建增多.
在创建实例之前就已经有了, 即在初始化类时就已有了. 存在Metaspace里 (Metaspace存储元信息) (占用内存的时间最久, 最不能有效利用内存空间)
类方法/ 静态方法: 一个静态方法隶属于类, 专门用于处理类变量的计算, 通过类名称来调用, 而无需实例化一个对象来调用, 必须加上一个static修饰词.
静态方法可以访问更改静态变量, 但不能直接访问非静态的成员(变量和方法).
在静态方法中,this(表示当前对象的引用) ( 当需要明确指出当前对象的引用时, 才使用this关键字, 如果是在同一个类中, 那么会自动加, 不要滥用this, 为了代码可读性! 原则上, 只有当类中变量和成员变量重名时才使用this指针 ) 和super(表示当前对象的相应父类对象引用) 无法使用. (因为this/ super所代表的对象还没产生, 既然没有对象, 就不会有实例变量.)
(变量引用) 类名或对象名与变量名之间为
.(方法引用) 类名或对象名与方法名之间为::(这个语法来自C++)这里的 this 可以认为就是类名(即构造方法名), 会看到this.xxx 或 this() 调用构造方法等等, 其实this 替换类名 (类的构造方法名) 类似于字面上替换. (super同理)
静态语句块
1
2
3
4
5
6
7
8
9
10
11
12class A1 {
static int a;
static int b;
static { // 静态语句块
a = 1;
b = 2;
}
public static void main(String args[]) {
System.out.println("hello");
new A1();
}
}- 静态语句块用来初始化静态变量
- 静态语句块不能访问非静态成员
- 静态语句块在main方法之前被调用
静态语句块用于初始化静态变量, 那么什么语句块用于初始化成员变量? -- 非静态语句块 (实例语句块), 会在执行某一个构造方法之前就执行, 是与静态块类似的语法.
1
2
3
4
5
6
7
8
9
10
11class A2 {
int a;
int b;
{ // 语句块
a = 1;
b = 2;
}
public static void main(String args[]) {
new A2();
}
}这一语法是匿名内部类初始化所必需的.
record类 (本条参考 - Java 17 updates) :
我们常常会写这样的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public final class Rectangle {
private final double length;
private final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double length() { return this.length; }
double width() { return this.width; }
// Implementation of equals() and hashCode(), which specify
// that two record objects are equal if they
// are of the same type and contain equal field values.
public boolean equals...
public int hashCode...
// An implementation of toString() that returns a string
// representation of all the record class's fields,
// including their names.
public String toString() {...}
}有了record关键字,我们可以这样写:
1
2// 就这一行代码
record Rectangle(double length, double width) { }record对于创建小型不可变对象很有帮助.
record classes 提供了 1. private final修饰的成员变量和相应的公共访问方法 (get()方法) 2. 全参的构造方法 3. equals/ hashCode/ toString方法.
record类是final的, 即不可被继承.
record类可以用作简单的 (不可变的) 数据容器. (serve as a simple "data carrier")
我们知道class类可以在文件中声明,也可以在类中声明/ 方法中声明。那么record类也一样,它有这几种方式:
直接在文件中声明的类:
1
public record range(int start, int end){}
内部类:
1
2
3public class DidispaceTest {
public record range(int start, int end){}
}方法内部类:
1
2
3
4
5public class DidispaceTest {
public void test() {
public record range(int start, int end){}
}
}可变参数 -- 语法糖, 类型后加
...来表示. 必须放在参数列表最后, 实际会被编译器当成数组. 不同的是, 类似String... trailing的参数声明可以接受空参数, 这带来了一定的灵活性.1
2
3
4
5
6
7
8
9
10public static void f(Integer... args) {
for (Integer i : args) { // Java 11 支持 for (var i : args) {} 的写法.
System.out.println(i + "");
}
}
public static void main(String[] args) {
f();
f(1);
f(2, 3, 4);
}如果使用方法重载 (特设多态), 那么你应当在至多一个重载方法上使用可变参数列表.
Java-Lesson 5.1 (inheritance)
继承
Java通过关键字extends来表达继承关系.
class SubClass extends SuperClassName {}Java只支持单继承, 只有一个"直接"父类. 父类的父类也是该子类的父类, 但不是直接父类, Java中所有类都是java.lang.Object 的子类.
实际编写代码中, 继承最好不要超过3层.
重新定义父类中的变量, 父类中相应的变量被隐藏 (hidden). 可以通过super关键词来访问这些隐藏的变量, 当然static的变量可以通过类名来访问. (隐藏和覆盖, 意思都是是其虽然被改写了, 但是依然可以通过super来调出来, 其区别在于运行前确不确定)
无论是什么类型的变量被重新定义都叫隐藏, 静态方法被重新定义也叫隐藏. (隐藏在编译时就分开了)
重新定义签名相同的成员方法 (前提是这些方法可达), 就是方法重写 (Overriding). (重写在编译时是分不开的)
定义一个与private的方法同名的方法也不叫隐藏 (因为本来就不可达) , 也不叫覆盖. 但是两者依然是分开的, 可以认为是天然的隐藏起来了.
只有既没有static也没有private修饰的成员方法被重新定义才是覆盖/ 重写 (Overriding)
隐藏和重写的区别
重写对应==运行时==, Java会在运行时判断哪个方法会被调用
(It is for non-static methods.)
隐藏对应编译时, Java在编译阶段就已经确定好了调用的对象 (即静态和实例变量/静态方法)
early binding/ late binding 早绑定 (编译时) / 迟绑定 (运行时)
在Java中, 任何类的构造方法, 第一行语句必须是调用父类的构造方法.
如果没有明确地调用父类的构造方法, 编译器会帮我们自动加一句super();
// 如果有参数, 那么只能自己写super(3);
构造方法无法继承, 它隶属于特定的类. 因此, 如果即使类没有写构造方法, 那么其也会有一个默认的构造方法, 而不是继承于父类的构造方法.
什么样的类成员会被子类继承?
- 私有的 (private) 类成员不能被子类继承
- 构造方法不能被继承
- 公共的 (public) 和保护性的 (protected) 类成员能被子类继承, 且子类和父类可以属于不同的包
- 无修饰的父类成员, 仅在同一个包中才能被子类继承
重写可继承的函数时, 其访问权限不能比父类中被重写方法的访问权限更低, 父类protected, 子类可为public.
重写可继承的函数时, 返回值如果是原始类型, 必须与原函数一致,如果是对象类型, 必须是原函数返回值或是其子类
以上2条只是对重写, 若是隐藏则无以上限制.
函数重载即特设多态 (Ad hoc polymorphism), 重载是不同的函数,因为虽然函数名相同但是参数不同.
⽽重写(也叫覆盖)是重新定义⽗类中签名相同的函数. 重载是特设多态, 重写是⼦类型多态. 重载在函数调⽤时所调⽤的具体函数 (函数绑定) 在编译时确定(early binding), 重写的函数绑定在运⾏时(late binding).
重载是重复利用名称, 而重写是重复利用方法的架构.
子类每个对象也是父类的对象 (is-a), 可以直接用子类对象赋给一个父类常量. 这种转化叫作: 向上转型 (upcasting)
父类的对象不能直接赋给子类. 但是可以强制转换 ( downcasting ). 想要安全的进行向下转型 ( 避免运行时产生异常 "ClassCastException" ) 时, 一般需要用运算符instanceof (判断实例是不是一个类/ 接口的子类) 来进行判断.
1
2if (animal instanceof Dog)
Dog dog = (Dog) animal;
Java-Lesson 6 (Generics)
作为一种静态类型语言, Java对参数需要固定类型, 虽然增加了安全性, 但却失去了灵活性. 通过引入“泛型” (generics) 的概念, Java可以将类型也作为一种参数, 从而实现参数多态 (参数不同, 逻辑一致).
泛型的语法:
泛型类:
[modifiers] class className <T, E, ...> {...}泛型接口:
[modifiers] interface interfaceName <T, E, ...> {}泛型方法:
[modifiers] <T, E, ... > returnType *functionName* ( parameterList ) {...}形式类型参数: 在定义类/ 接口或方法时使用的类型参数. 可以用任意字母来表示, 如T, E, K, V等, 常用T, 需要加尖括号. (这对尖括号<>叫菱形运算符 / 钻石运算符)
泛型类:
1
2
3
4
5
6
7
8
9class Stack<A> { // 形式类型参数
void push(A a) {...}
A pop() {...}
public static void main(String[] args) {
Stack<Integer> s1 = new Stack<>(); // 使用时需要加上实际的数据类型
// 你也可以使用 class Stack<T, E, F> {} 以支持有3种变量类型元素的栈
}
}泛型接口: 与泛型类类似.
public class Computer<T extends Disk> {...}T是Disk的一个子类, 通过这样的方式对类型作限制. 此处的 Disk 被称为该形式类型的限定类型 (bounding type)泛型方法:
1
public <T> void f(T x) {...}
1
2
3
4
5
6
7public <E, V> void display(E[] list1, V[] list2) {
for (E e : list1)
for (V v : list2) {
System.out.println(e);
System.out.println(v);
}
}在返回类型之前加一个
<E, V>(形式类型参数表, 后续可以使用于参数/ 函数体)形式类型参数和普通类型用法一样.
实际类型参数, 必须为对象类型. 在构造对象类型时, 可显示声明, 也可缺省.
1
GenericsSample<Integer, Long, List> t2 = new GenericsSample<Integer, Long, List>();
1
GenericsSample<Integer, Long, List> t2 = new GenericsSample<>();
这件事情 (泛型机制使得获取一个泛型返回类型时不用再人为向下转型) 听起来很完美, 但是事实上这只是因为编译器帮你做了转型, 是编译时刻的语法糖, 真正在运行时类型信息被擦掉了. 证据: 观察字节码可知, 泛型的实际类型根本就没有在字节码中出现, 而是以java.lang.Object (或是一个父类) 代替了.
泛型背后的机制: 其实Java的泛型都是伪泛型, 其为了能够后向兼容Java旧时代 (Java 4.0及以前, 当时还没有泛型) 代码, 并不是运行时进行泛型的支持, 而是通过编译器在编译阶段对类型进行擦除. (都被擦除成Object) 如果类型是受限的, 则会替换为其限定类型.
如果是这样的泛型, 观察字节码则会看到, 类型参数被擦除到了边界类型 (父类Disk). 事实上, Java泛型的实现方式即是边界类型替换.
1
2
3public class Computer<T extends Disk> {
// ...
}所以, 如果泛型的类型不匹配, 如
Holder<Integer> holder = new Holder<>(); holder.set("string");, 在编译期就会fail, 无法通过编译.通过泛型背后机制 (伪泛型) 理解问题:
为什么泛型的实际参数类型不能是原始类型 (因为都会被擦除为Object或其限定类) 所以
List<Integer>是可以的而不能是int.为什么 instanceof 判断不了泛型, 比如:
arg instanceof T或T<String> a = …; a instanceof T<Integer>;// Error为什么不能用泛型创建对象, 即 T a = new T(); (因为本质上就是new Object()) // Error
为什么不能用泛型创建数组对象, 即 T[] a = new T[size];// Error
为什么不能声明静态的泛类型的变量,如:
1
2
3public class Singleton<T> {
public static T singleInstance; // ERROR
}擦除后就是Object类型. 导致用
Singleton<Integer>和Singleton<String>所生成的静态变量都是Object类型, 都是存在相同的data space里面的Object, 从而没有意义 (产生了冲突) !而声明泛型的非静态的成员变量显然是可以的.
既然这些行为都不能做, 那么泛型到底还有啥用?
泛型的语法会让编译器帮我们做各种检查: 编译器会做类型检查, 防止实参与要求的泛型实际类型不匹配. (注意, 由于自动包装机制, int类型会被自动转换为Integer).
编译器还会做其他事情: 将所有的T都替换成边界类型 (常为Object); 涉及泛型作为返回值, 编译器会自动插入一个向下转型的操作.
字节码层面是看不见"泛型"的 (没有"泛型"的概念) , 泛型只是编译器层面提供的一套语法功能, 所以使用了泛型的代码完全可以在旧版本JVM上跑.
如果真的想生成泛型对象, 怎么办?
- 使用工厂创建对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Holder<T> {
private T t;
public void init(IFactory factory) {
this.t = factory.create(); // 实现新建对象
}
}
interface IFactory<T> {
T create();
}
class IntegerFactory implements IFactory<Integer> {
public Integer create() {
return new Integer(10);
}
}
public class newTwithFactory {
public static void main(String[] args) {
Holder<Integer> holder = new Holder<>();
holder.init(new Integerfactory()); // 给holder一个具体的工厂
}
}- 使用RTTI (运行时类型识别)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Holder<T> {
private T t;
private Class<T> kind;
public Holder(Class<T> kind) {
this.kind = kind;
}
public void init() {
try {
this.t = kind.newInstance(); // 这样的生成实例的写法要求kind必须有一个无参构造器
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Holder<Integer> holder = new Holder<>(Integer.class);
holder.init();
}
}多个边界 (Bounds) -- 这里传递出的信息是, 在表示边界时的 extends 关键字与表示类继承时的含义不一样, 表达边界时允许使用
&来表达还实现了若干个接口.Java只允许继承一个父类, 但是可以实现多个接口, 这使得泛型语法可以有多个边界.
1
2
3
4interface HasColor { java.awt.Color getColor(); }
class Dimension { public int x, y, z; }
// ColoredDimension 里面的泛型类型T既继承了Dimension又实现了HasColor接口.
class ColoredDimension <T extends Dimension & HasColor> {...} // & 用来分隔多个边界.注意: 如果既有父类又有父接口, 那么仅有的那一个父类一定放在多个边界的第一个, 后面跟着的是若干个父接口. (为什么? 因为父类很特殊, 至多有一个, 其放在最前面对编译检查是有利的. )
例子:
1
2
3
4
5
6
7
8
9
10
11class Fruit {}
class Apple extends Fruit {}
class Plate<T> {
private T item;
public Plate(T t) {item = t;}
public void set(T t) {item = t;}
public T get() { return item;}
}
// 尝试创建一个装苹果的盘子
Plate<Fruit> p = new Plate<Apple>(new Apple()); // Compile Error!装苹果的盘子无法转换成装水果的盘子 (不是父类子类关系!) , 即使苹果是一种水果.
回到父类和子类的最初的定义: 子类是 父类的特例, 父类具有的特性子类必须都要有.
所以, 装苹果的盘子不是装水果的盘子的子类, 它不具备装水果的盘子的性质 -- 可以装任何水果 (香蕉, 蛇果, 车厘子, ...) , 他们没有一个是另一个的特殊化的关系.
协变 (构造的复杂类型后保持原来的父子关系), 逆变 (仍然有父子关系, 但是与原来相反) , 不变 (没有父子关系) . (根据上一条所述, 水果 -> 装水果的盘子 是一种不变构造)
如何解决以上例子中的问题?
1
2
3// 表示的是一个能放任意**一种**水果以及任意**一种**水果派生类的盘子
Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
List<? extends Fruit> flist = new ArrayList<Apple>();泛型通配符
?到底表达什么意思?答: 表示一定是一个确定的具体的类型, 但是具体这个类型是什么, 不确定.
比如一个装苹果的盘子, 是装一种确定的水果的盘子的子类, 所以上一条中的写法是可通过编译的.
通配符带来的意思是a plate will hold "some specific (特定的) type of fruit which is not specify (不确定)".
所以, 一个装任意一种特定类型水果的盘子不能再被存入任何元素, 无论是new Fruit() 还是 new Apple(), 因为编译器的能力无法确定这个
?表示的确切类型是哪一种, 为了安全性, 编译器不会再允许放入任何元素.1
2
3
4
5
6
7Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
// 不能再存入任何元素
p.set(new Fruit()); // Error!
p.set(new Apple()); // Error!
// 读取出来的东西只能放在Fruit或它的基类里
Fruit newFruit1 = p.get();
Object newFruit1 = p.get();超类通配符 (supertype wildcard)
1
2
3
4
5
6
7Plate<? super Fruit> p = new Plate<Apple>(new Fruit()); // 只能初始化为Fruit及其超类
// 可以存入任何Fruit类及其子类
p.set(new Fruit());
p.set(new Apple());
// 读取出来的东西只能放在Object类里 (编译器不知道你这个超类有多超, 只能保守估计)
Object newFruit1 = p.get();
Fruit newFruit1 = p.get(); // Error!泛型的类型通配符 (Wildcard)
之前一个泛型对象名只能引用同一种泛型对象, 如GeneralType<String> a 只能指向GeneralType<String>的对象 (有时候不希望list<A>和list<B>无任何关系)
如果要使用同一个泛型对象名去引用不同的泛型对象, 就需要使用通配符 “?” 创建泛型类对象
但要求不同泛型对象的类型实参必须是某个类或者其子类, 或实现某个接口泛型类名 <? extends T> 0 = null;
除了可以利用extends限定实际类型参数是某个类型的子类外 (设置上限), 还可以用super 限定其是某个类的父类 (设置下限)
1
泛型类型 <? super anyclass> x = null;
例子: (extends 和 super 刚好是相反的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class GeneralType <T> {
T obj;
public void setObj (T obj){
this.obj = obj;
}
public T getObj() {
return obj;
}
}
GeneralType <? extends Number> x = null; // 类型通配符创建类, 并实例化为x
x = new GeneralType <Long> ();
x = new GeneralType <Integer> ();
Number a = x.getObj(); // Correct
x.setObj(Integer.valueOf(1)); // Error, 因为x类型并不可判定(编译器为了Safe而不让x写入)
GeneralType <? super Integer> x = null;
x = new GeneralType <Object> ();
x = new GeneralType <Number> ();
Number a = x.getObj(); // Error, x到底是什么类编译器不可判定, 为了safe-不能把超类赋值给子类
x.setObj(Integer.valueOf(1)); // CorrectJava==泛型==举例:
1
2
3
4
5
6
7
8
9
10
11List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
void printArray( List<?> data ) {
for (i = 0; i <data.length(); i++) {
System.out.println(data.get(i));
}
}
printArray(name)
printArray(age)
printArray(number)Self-bounded types 自限定类型
1
class SelfBounded<T extends SelfBounded<T>> {}
目的是实现协变或者逆变 (参数协变(重载或者自限定), 返回值协变(Java 5自动支持) ), 非常漂亮的设计.
自限定的限制只服务于强制继承关系, 表示该类使用的类型参数和使用该参数的类是同一种基类.
也可以将自限定用于泛型方法:
1
static <T extends SelfBounded<T>> T f(T arg) {}
使用自限定类型自然地支持参数协变类型 (covariant argument type):
1
2
3
4
5
6
7
8
9
10
11interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T arg);
}
// 这里的set方法接收确切类型参数Setter
interface Setter extends SelfBoundSetter<Setter> {}
public class SelfBoundingAndCovariantArguments {
void test(Setter s1, Setter s2) {
s1.set(s2);
}
}Java Optional 类, 更优雅处理null.
创建 Optional 对象
1)可以使用静态方法
empty()创建一个空的 Optional 对象1
2Optional<String> empty = Optional.empty();
System.out.println(empty); // 输出:Optional.empty2)可以使用静态方法
of()创建一个非空的 Optional 对象1
2Optional<String> opt = Optional.of("name");
System.out.println(opt); // 输出:Optional[name]当然了,传递给
of()方法的参数必须是非空的,也就是说不能为 null,否则仍然会抛出 NullPointerException。1
2String name = null;
Optional<String> optnull = Optional.of(name);3)可以使用静态方法
ofNullable()创建一个即可空又可非空的 Optional 对象1
2
3String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull); // 输出:Optional.emptyofNullable()方法内部有一个三元表达式,如果为参数为 null,则返回私有常量 EMPTY;否则使用 new 关键字创建了一个新的 Optional 对象——不会再抛出NPE异常了。判断值是否存在
可以通过方法
isPresent()判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false——取代了obj != null的判断。1
2
3
4
5Optional<String> opt = Optional.of("name");
System.out.println(opt.isPresent()); // 输出:true
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(opt.isPresent()); // 输出:falseJava 11 后还可以通过方法
isEmpty()判断与isPresent()相反的结果。非空表达式
Optional 类有一个非常现代化的方法——
ifPresent(),允许我们使用函数式编程的方式执行一些代码,因此,我把它称为非空表达式。如果没有该方法的话,我们通常需要先通过isPresent()方法对 Optional 对象进行判空后再执行相应的代码:有了
ifPresent()之后,可以直接将 Lambda 表达式传递给该方法,代码更加简洁,更加直观。1
2Optional<String> opt = Optional.of("name");
opt.ifPresent(str -> System.out.println(str.length()));Java 9 后还可以通过方法
ifPresentOrElse(action, emptyAction)执行两种结果,非空时执行 action,空时执行 emptyAction。1
2Optional<String> opt = Optional.of("name");
opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("为空"));设置(获取)默认值
有时候,我们在创建(获取) Optional 对象的时候,需要一个默认值,
orElse()和orElseGet()方法就派上用场了。orElse()方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值得类型一致。1
2
3String nullName = null;
String name = Optional.ofNullable(nullName).orElse("name");
System.out.println(name); // 输出:nameorElseGet()方法与orElse()方法类似,但参数类型不同。如果 Optional 对象中的值为 null,则执行参数中的函数。1
2
3String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(()->"name");
System.out.println(name); // 输出:name获取值
直观从语义上来看,
get()方法才是最正宗的获取 Optional 对象值的方法,但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖。建议
orElseGet()方法获取 Optional 对象的值。过滤值
新的任务:用户注册时对密码的长度进行检查。
Optional 类的
filter()方法,就派上了用场。1
2
3
4
5
6
7public class FilterOptionalDemo {
public static void main(String[] args) {
String password = "12345";
Optional<String> opt = Optional.ofNullable(password);
System.out.println(opt.filter(pwd -> pwd.length() > 6).isPresent());
}
}filter()方法的参数类型为 Predicate(Java 8 新增的一个函数式接口),也就是说可以将一个 Lambda 表达式传递给该方法作为条件,如果表达式的结果为 false,则返回一个 EMPTY 的 Optional 对象,否则返回过滤后的 Optional 对象。1
2
3
4
5
6
7
8/* 过滤长度6~10的密码 */
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
password = "1234567";
opt = Optional.ofNullable(password);
boolean result = opt.filter(len6.and(len10)).isPresent();
System.out.println(result);转换值
1
2
3
4
5
6
7
8
9
10public class OptionalMapDemo {
public static void main(String[] args) {
String name = "name";
Optional<String> nameOptional = Optional.of(name);
Optional<Integer> intOpt = nameOptional
.map(String::length);
System.out.println( intOpt.orElse(0));
}
}在上面这个例子中,
map()方法的参数String::length,意味着要 将原有的字符串类型的 Optional 按照字符串长度重新生成一个新的 Optional 对象,类型为 Integer。把
map()方法与filter()方法结合起来:1
2
3
4
5Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
Predicate<String> eq = pwd -> pwd.equals("password");
boolean result = opt.map(String::toLowerCase).filter(len6.and(len10).and(eq)).isPresent();
System.out.println(result);
Java-Lesson 7 (FP & Interface)
为什么要函数式编程? 因为如果没有函数式, 单纯面向对象, 一切都是名词, 基于名词去做某件事常常是不必要的, 这就导致Java代码中出现很多的不必要的繁杂繁冗. (函数式编程是一种声明式的编程风格, 函数是没有副作用的, 外部的状态不因函数的执行而改变, 这带来的好处就是使得并发度尽可能的高).
lambda表达式也是语法糖, 在JVM上, 一切都是类和对象.
Java Lambda表达式和使用函数作为参数:
1
2
3
4
5
6
7
8public int[][] calculate(BiFunction<int[][], int[][], int[][]> func) {
func.apply(A, B); // 这里BiFunction为一个双参数的函数(单参数去掉Bi), 它被作为参数传入, 以待调用
}
public int[][] plusFromConsole() {
return calculate(this::plus); // 所在类里面的一个方法plus, 作为参数传入
// return calculate((a, b)->plus(a, b)); // lambda写法
}lambda表达式:
(para1, para2, ...) -> { code block }1
2
3
4() -> {
System.out.println("a");
System out.println("b");
}例如:
1
2(int a, int b) -> { return a + b; }
() -> System.out.println("Hello Java");语法糖:
参数类型可以明确声明, 也可省略, 编译器根据上下文推断.
只有一个参数时, 圆括号可省略;
只有一条语句, {}可省略. 如:
a -> a + 1函数式接口
- 函数式接口是只包含一个抽象方法声明的接口
- 每个lambda表达式 (或一个形如
A::foo&a::foo的方法引用, 本质上方法引用也是lambda表达式的简化) 都能隐式地赋给函数式接口.
java.lang.Runnable就是一种函数式接口, 在Runnable接口中只声明一个抽象方法void run()其他常见的函数式接口
1
2
3
4
5
6// Consumer<E> 函数式接口, 消耗一个E类型, 返回void.
Consumer<Integer> c = (int x) -> {System.out.println(x) };
// BiConsumer<K, V> 和Consumer类似
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
// Predicate<E> 预测. 参数是E, 返回布尔值
Predicate<String> p = (String s) -> { s == null };定义自己的函数式接口
@FunctionalInterface是Java 8新加入的一种接口, 用于指明该接口类型声明是根据Java语言规范定义的函数式接口.1
2
3
4
public interface WorkerInterface {
public void doSomeWork();
}java.util.function包里的函数式接口:
Interface Function<T, R>实际使用举例:
使用现成的函数式接口:
1
2
3Function<Integer, Integer> myFunc = a -> a + 1;
myFunc.apply(1);
System.out.println(myFunc.apply(1)); // 2自己定义需要的函数式接口并使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Func {
public int apply(int a, int b, int c, boolean bl);
}
public class Main {
public static void main(String[] args) {
Func fc = (a, b, c, bl) -> {
if (bl) {
return -1;
} else {
return a * b * c + 114514;
}
};
System.out.println( fc.apply(1, 0, 1, false) );
}
}如果要用函数式编程的方式实现递归, 则为了避免使用未初始化变量, 需要运用字段来作为递归的函数式接口的声明. (对于纯函数的支持在这一点上不如Python, 然而Python的函数式也不够纯 (>_<) )
构造方法 (new) 的方法引用, 例如
MakeNoArgs mna = Dog::new;然后就可以通过mna,apply()创建新Dog对象.Java 8提供了有限但是还算可以的闭包支持, 也就是实际上lambda表达式也可以改变一些可达的变量. 到底哪些是可改变的? 1. 外围类的字段. 2. final的局部变量或者是实际上final的局部变量 (如
List<Integer> l = new ArrayList<>(); l.add(5);, l这个局部变量的指向实际上没有变, 尽管指向的对象自己发生了一些改变.)方法重载 Method Overloading:
方法重载可以让我们使用相同的方法名, 来处理不同的参数, 比如下面这些方法:
1
2
3
4
5
6
7
8
9public int sum(int num1, int num2) {
...
}
public float sum(float num1, float num2) {
...
}
public double sum(double num1, double num2) {
...
}每个类中可以有一个特殊的构建函数, 它用于初始化实例. 构建函数是一个实例被创建时最先被调用的函数, 每次创建实例的时候, 它的构建函数都会被调用.
1
2
3
4
5
6
7
8
9
10public class Student {
String name;
public Student() {
name = "Samuel";
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.name);
}
}
上例中, Student就是构建函数, 构建函数不能有返回值, 而且函数名必须和类名一致.
可以在类中创建了两个同名函数, 但是两者参数不同, 这就是重载 (Overload).
修饰访问权限的关键词是 public/ private/ protected. 被定义为public的class, attributes, method可以被任何类访问, 如果是private那么就无法被其他类访问, protected适用于继承关系间的类, 被定义为protected的属性和方法可以被子类访问.
Java继承 子类名
extends父类名数据抽象可以通过 abstract class 或 interface (接口) 实现. abstract 关键字是一个用于类和方法的修饰符, 我们无法创建 abstract class 类型的实例 ( 不能用new运算符 ), 这种抽象类只能被继承. 而 abstract method 只能定义在 abstract class 中, 这种方法没有具体执行内容. 一个抽象类既可以有抽象方法也可以有正常的方法:
如果一个类中没有包含足够的信息来描绘一个具体的对象 (方法没实现), 这样的类就是抽象类. 在抽象类⾥, 不定义⽅法体, 只需声明不实现的方法 -- 抽象方法.
1
2
3
4
5
6
7
8
9
10
11
12
13/** 抽象类只能被继承 */
public abstract class Person {
public abstract void greet();
public void sleep() {
System.out.println("Zzz");
}
}
public class Teenager extends Person {
public void greet() {
System.out.println("I am a teenager.");
}
}抽象类的子类可以是抽象类, 如果子类不是抽象类, 那么其必须实现父类中的所有抽象方法.
抽象类不能用final修饰. (抽象类就是为了继承, 而final不允许继承)
抽象类中不一定包含抽象方法, 但包含抽象方法的类一定为抽象类.
接口 Interface
另一个实现数据抽象的方式就是使用接口 (interface). 一个接口就是完全的抽象类, 其中
只含有抽象方法 (不含一般的方法) , 这些方法中是没有任何逻辑代码的. 类的主要作用便是定义一些特定的方法, 具体逻辑让正常的类实现.可以认为, 接口就是一个抽象程度很高的基类.
1
2
3
4interface Student {
public void goToSchool();
public void takeExam();
}接口没有构造方法.
接口的变量都是 static final 修饰的, 缺省的也是静态常量, 所以必须赋初始值. (静态常量就是只有一份, 常量是没有一个实例就有一份, 一般常量一份就够了, 所以常常用static final)
接口的成员可访问性都是==public (所以子类相应的成员必须也是public)== (缺省也是public)
接口中除了抽象方法之外, 还可以含有静态方法和default (默认方法) . default方法中的实现会被所有implement了该接口的子类所具有.
如果要使用接口的方法, 那么具体的类必须实现 ( implements ) 其接口. 只要被实现, 具体的类必须将接口方法的具体逻辑全部实现:
1
2
3
4
5
6
7
8public class Person implements Student {
public void goToSchool() {
System.out.println("I'm going to school.");
}
public void takeExam() {
System.out.println("I'm taking an exam.");
}
}注意, 在接口中, 缺省的成员方法也是public, 但是在实现中, 每个方法的public修饰不能省, 否则会编译失败.
一个类可以实现 (implement) 多个接口 (interface) , 如:
public class Person implements Student, Employee {...}==接口可以作为一种引用类型 (静态类型) 使用==, (但是万万不可作为实际类型) 可以声明接口类型的变量或数组, 并用它来访问实现该接口的类的对象.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public interface IShape {
public static final double PI = 3.14;
double getArea();
public abstract double getLength();
public static void showPI(){
System.out.println(PI);
}
public default void getInfo(){
System.out.println("这是⼀个图形");
}
}
public class CireCleForI implements IShape{
double radius;
public CireCleForI(double r) {
this.radius = r;
}
public double getArea() {
return PI*radius*radius;
}
public double getLength() {
return 2*PI*radius;
}
}
public class TestIShape {
public static void main(String[] args) {
IShape cir = new CireCleForI(2.2);
System.out.println("⾯积 : "+ cir.getArea());
System.out.println("周⻓ : "+ cir.getLength());
cir.getInfo();
IShape.showPI();
}
}接口继承:
- 接口可通过extends关键字声明该新接口是某个已存在的父接口的子接口, 它将继承父接口的所有变量与方法 (静态方法除外, 静态方法只能通过接口名来访问)
- 接口支持多继承 (一个接口可以继承多个接口, 接口不可以继承类, 多个父接口用逗号隔开)
- 如果接口中定义了与父接口同名的常量或相同的方法, 则父接口中的常量和静态方法被隐藏, 默认方法和抽象方法被重写.
可以利用接口继承轻松地为接口创建一个别名:
1
interface G extends F {} // 接口G是接口F的别名.
利用default方法实现混入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public interface Flyable {
default void fly() { // 如果不用default的话则不能给出内部实现
System.out.println("I can fly!");
}
}
public interface Swimmable {
default void swim() {
System.out.println("I can swim!");
}
}
public class Duck implements Flyable, Swimmable {
}注意: 接口里面所有方法默认是public访问修饰的, 这里default关键字不是访问修饰符, 而是default关键字.
接口多继承中的名字冲突问题
- 接口的多重继承中可能存在常量名或方法名重复的问题, 即名字冲突问题
- 对于常量, 若名称不冲突, 子接口可以继承多个父接口中的常量, 但如果多个父接口中有同名的常量, 则必须通过 接口名.常量名 区分.
- 对于多个父接口中存在同名的方法包含默认方法 (default) 时, 也会发生命名冲突, 这时不能通过 接口名.默认方法名 来解决. (显然是不能的, 因为default是实例方法, 都没有实例, 哪里有这个方法呢?) 必须要在当前类中自己新定义一个同名的方法来覆盖才行.
- 而此时如果想要调用被覆盖的方法, 要用父接口名.super.方法名 (一般是super.方法名, 这里加一个父接口名是为说清楚到底是哪个父接口)
如果发生了继承类和实现接口的命名冲突, 那么“类”优先, 继承的父方法中的同名方法. (先认为子类的方法是所继承的父类的方法)
继承代表的类之间的关系是
is-a关系, 而接口的类之间关系则是has-a关系 (组合)
Java-Lesson 8 (Stream)
查看变量所在类和类型:
1
2
3
4Integer a = 0;
System.out.println(a.getClass());
System.out.println(a.getClass().getSimpleName());
System.out.println(a.getClass().getTypeName());Java迭代器
1
2
3
4
5
6
7
8Iterator<String> i_ci = cities.iterator();
while( i_ci.hasNext() ) {
System.out.println(i_ci.next());
}
Iterator<Integer> i_nu = numbers.iterator();
while( i_nu.hasNext() ) {
System.out.println(i_nu.next());
}迭代器的坑: 不要在列表迭代中对原来的迭代器作改变 (删除).
1
2
3
4
5List<Integer> numbers = new ArrayList<Integer>(Arrays.asList(100, 200, 300));
for (Integer num : numbers) {
numbers.remove(num); // dangerous!!! mutates the list we're iterating over
}
System.out.println(numbers); /* list empty here? */正确的做法是, 新建一个数组, 过滤掉原来的. (本质是新建而不是删除)
1
2
3
4
5List<Integer> newList = new ArrayList<Integer>();
for (Integer num : numbers) {
if (num > 100)
newList.add(num);
}一个正确但是不推荐的做法: (利用迭代器) (不推荐是因为 -- 多个迭代器指向同一个对象时出问题; 多线程会出问题)
1
2
3
4
5
6
7List<Integer> numbers = new ArrayList<Integer>(Arrays.asList(100, 200, 300));
Iterator<Integer> iter = numbers.iterator();
while (iter.hasNext()) {
Iterator num = iter.next();
if (num <= 100)
iter.remove(); // 此处迭代器会无误地指向删除元素的下一个, 从而正确完成过滤的任务
}函数组合
andThencomposeandornegate流的常用操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/* map */
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> squaresList = numbers.stream().map( i -> i * i).collect(Collectors.toList());
squaresList = numbers.stream().map( i -> i * i).distinct().collect(Collectors.toList()); // distinct去重复值
/* filter */
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
long count = strings.stream().filter(string -> string.isEmpty()).count();
/* sorted */
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> sortedList = numbers.stream().sorted().collect(Collectors.toList());
List<Integer> sortedList = numbers.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
numbers.stream().sorted((a, b) -> {return a.compareTo(b);}).forEach( n -> System.out.println(n) );
/* reduce */
int reduced = IntStream.range(1, 4).reduce((a, b) -> a + b).getAsInt();
/* Iterate */
Stream<Integer> stream = Stream.iterate(0, (x) -> x + 2).limit(6);
stream.forEach(System.out::println);
/* Match */
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
boolean allMatch = list.stream().allMatch(e -> e > 3); //false
boolean anyMatch = list.stream().anyMatch(e -> e > 3); //true
boolean noneMatch = list.stream().noneMatch(e -> e > 10); //true
/* Min, Max */
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Integer max = list.stream().max((a, b) -> a.compareTo(b)).get(); // 5
Integer min = list.stream().min(Integer::compareTo).get(); // 1创建一个String对象的"流", 将其中每一个对象交给filter过滤, 最后forEach()会对每个保留下来的对象应用println方法引用.
1
2Stream.of("bar", "foobar", "foobaz", "fongopuckey")
.filter(s -> s.length() < 5).forEach(System.out::println);流是一个与任何特定存储机制都没有关系的元素序列, 事实上, 我们说流"没有存储".
借助流显示随机的5-20之间的整数:
1
2
3
4
5
6
7
8
9
10
11
12
13// stream/Randoms.java
import java.util.*;
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct()
.limit(7)
.sorted()
.forEach(System.out::println);
}
}先为Random对象设置一个种子(这样程序每次运行都会得到相同的结果). ints() 方法会生成一个流, 两个参数可以设置所生成值的上下界. 这里生成了一个由随机的int组成的流, 我们使用中间流操作distinct()去掉重复的值, 再使用limit()选择前7个值. 然后我们告诉它, 希望元素是有序的(sorted()). 最后, 我们使用了 ForEach(), 它会根据我们传递的函数, 在每个流对象上执行一个操作. 这里我们传递了一个方法引用
System.out::println, 用于将每个条目显示在控制台上.collect会将将流 (惰性) 收集起来:
1
2
3Predicate<Use> predicate1 = user -> user.getAge() < 21;
List<User> collect = list.stream().filter(predicate1).filter(user -> user.getSalary() > 2000).collect(Collections.toList()).forEach(System.out::println);
// 构建流, 过滤, 收集器收集, 逐个打印foreach是对列表操作, map / reduce则是对流操作:
1
2
3
4/** map */
list.stream().map(num->num + 10);
/** reduce */
int sum = list.stream().reduce(0, (a, b) -> a + b).get()流还有
distinct()操作, 把相同的元素变为一个. 还有sorted(Comparator.reserveOrder())Iterate 迭代器
1
2
3
4
5Stream<Integer> stream = Stream.iterate(0, (x) -> x + 2).limit(6); // 前项推后项, 从0开始
stream.forEach(System.out::print);
/**
* Output: 0246810
*/
Java-Lesson 9 (Reflection)
Introspection 自省: 运行时观察到类型信息
Reflection 反射: 不仅是检查当前对象的类型, 还可以修改对象的一些性质 (控制改变).
反射的核心是JVM在运行时动态加载类或调用/访问属性, 它不需要事先 (静态编译器) 知道运行对象是谁.
反射 (Reflection) 反射是一种在运行时可以检视自身程序和操纵程序内部属性的一种语言特性 (不是java独有的). 比如对于Java而言, 反射可以使其运行时动态的加载类并获取类的详细信息, 从而可以操作类和对象的属性和方法.
反射给静态语言Java带来了动态性. 反射机制允许程序在执行期借助反射API获得任何类的内部信息, 并能直接操作任意对象的内部属性和方法. (通过对象得到类的属性/ 方法/ 构造器...)
反射的坏处, 比正向直接执行相应的操作慢多了. (几十倍 - 上百倍) 关闭语言访问检查可以一定程度上快一些.
在加载完类后, 在堆内存区就产生了一个Class 类型的对象, 包含了完整的类的结构信息.
实体类 entity, 也称pojo (简单Java对象)
Class c = Class.forName("java.util.Stack");这里是大写Class, 是反射语法, Stack (栈) 这个类就被加载到 c 这个变量里面去了. 然后通过这个c 可以得到Stack的方法和变量. (c.getDeclaredMethods())反射举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.lang.reflect.*;
public class DumpMethods {
public static void main(String args[]) {
try {
Class c = Class.forName("java.util.Stack");
Method m[] = c.getDeclaredMethods();
for (int i = 0; i < m.length; i++)
System.out.println(m[i].toString());
}
catch (Throwable e) {
System.err.println(e);
}
}
}Class类的创建方式:
1
2
3
4
5Class c1 = person.getClass();
Class c2 = Class.forName("com.reflection.Student");
Class c3 = Student.class;
// 仅对内置类型的包装类
Class c4 = Integer.TYPE;
还可以用对象的 .getSuperClass() 方法.
很多类型都有Class对象: 各种类, 接口, 数组, 枚举, 注解, 基本数据类型 (包装类), void. 数组只要是同样类型同维度都是同一个Class.
通过反射调用 (而不是正向调用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import java.lang.reflect.*;
public class TestInvoke {
public int add(int a, int b) {
return a + b;
}
public static void main(String args[]) {
try {
Class cls = Class.forName("TestInvoke");
Class partypes[] = new Class[2];
partypes[0] = Integer.TYPE;
partypes[1] = Integer.TYPE;
Method meth = cls.getMethod("add", partypes);
TestInvoke methobj = new TestInvoke();
Object arglist[] = new Object[2];
arglist[0] = new Integer(37);
arglist[1] = new Integer(47);
Object retobj = meth.invoke(methobj, arglist); // 通过反射api调用方法
Integer retval = (Integer)retobj;
System.out.println(retval.intValue());
}
catch (Throwable e) {
System.err.println(e);
}
}
}
通过反射可以获取很多类的信息.
c是Class类型.
1
2
3
4
5
6
7
8
9
10
11
12
13
14Class c = A.getClass();
c.getName(); // 包名 + 类名
c.getSimpleName(); // 类名
c.getFields(); // public的属性
c.getDeclaredFileds(); // 所有属性
c.getDeclaredFiled("name"); // 获得name属性
c.getMethods();
c.getDeclaredMethods();
c.getMethod("get", null);
c.getMethod("set", String.class);
c.getDeclaredConstructors();反射获取类内的方法, 使用 方法名.invoke(...) 的方式, 以统一的方式调用类内函数.
反射可以用来创建类的实例对象. 可以对用反射创建的对象做很多事情.
1
2
3
4
5
6
7
8
9
10
11
12
13
14Class c1 = Class.forName("com.User");
User user = (User)c1.newInstance(); // 相当于调用一个无参的构造方法
// 调用有参的构造方法
Constructor constructor = c1.getDeclaredConstructor(String.class, int,class, int.class);
User user2 = (User)constructor.newInstance("chaos", 1, 18);
// 调用方法(通过invoke)
Method setName = c1.getDeclaredMethod("setName", String.class);
setName.invoke(user2, "kid"); // 第一个参数是隶属的对象, 第二个是所invoke方法的参数列表
// 操作属性
Field name = c1.getDeclaredField("name");
// 不能对private属性直接修改, 但是可以加上下面这条语句以关闭Java的语言访问检查
name.setAccessible(true);
name.set(user2, "Guy");通过反射获取泛型.
关于Java泛型,很多人都有一个误解,认为Java代码在编译时会擦除泛型的类型,从而在运行时导致没法访问其类型,这其实并不完全正确,因为有一部分泛型信息是可以在运行时动态获取的,这部分信息基本能够满足我们日常开发中的大多数场景,本节我们就来了解相关的知识。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static void getMethodReturnType() throws Exception{
Method method = MyClass.class.getMethod("getStringList",null);
System.out.println(method.getReturnType());
Type retrunType = method.getGenericReturnType();
System.out.println(retrunType);
if(retrunType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType)retrunType;
Type[] typeArguments = type.getActualTypeArguments();
for(Type typeArgument : typeArguments) {
Class typeArgClass = (Class)typeArgument;
System.out.println("泛型类型:" + typeArgClass);
}
}
}以上代码的关键在于
ParameterizedType表示一种参数化类型. Java引入了包括ParameterizedType的几种类型来支持反射操作泛型. 还有GenericArrayTypeTypeVariableWildcardType.反射操作注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// 创建2个注解
TableTest {
String value();
}
FieldTest {
String columnName();
String type();
int length();
}
class Student1 {
private int id;
private int age;
}
public class Reflection() {
Class c1 = Class.forName("com.Student1");
// 用反射获取注解信息
Annotation[] ann = c1.getAnnotations();
System.out.println(ann);
TableTest tabletest = (TableTest)c1.getAnnotation(TableTest.class);
String val = tabletest.value();
System.out.println(val);
Field name = c1.getDeclaredField("name"); // 获取字段
FieldTest annotation = name.getAnnotation(FieldTest.class); //获取注解
System.out.println(annotation.columnName());
}反射有什么用?
- Rapid Application Development (RAD)
- Visual approach to GUI development
- Requires information about component at run-time
- Remote Method Invocation (RMI)
- Distributed objects
重要用途是各种通用框架的开发; 编写分布式代码; ...
Java-Lesson 10 (Annotation)
注解 Annotation -- 可以被其他程序 (编译器) 识别 ; 注释 comment
内置注解
@Override重写(覆盖)@Deprecated弃用@SuppressWarnings()抑制警告 必须要有参数("all"),("unchecked"),(value = {"unchecked", "deprecation"})等参数可以有哪些呢? deprecation - 使用了过时的类或方法 ; fallthrough - switch块缺少了break ; finally - 任何finally字句不能正常完成 ; ... ; all - 所有情况
元注解 meta-annotation 注解的注解.
只有4个, 有6个, Java 8 新增了@Repeatable(用于定义可重复注解) 和@Native(表示变量可被本地代码引用)@Target用于描述注解的适用范围 有TYPE, FIELD, METHOD, PARAMETER, ...@Retention表示在什么级别上保存该注释信息, 用于描述注解的生命周期. 有SOURCE (源码), CLASS (字节码), RUNTIME. 一般是 RUNTIME@Documented该注解包含在javadoc中 -- 在javadoc生成api文档时, 被这个注解标记的元素在文档上也会出现该注解.@Inherited子类可以继承父类的注解, 只对目标为类/ 接口/ 枚举有效.自定义一个注解: (元注解派上用场)
1
2
3
4
5
6
7
8// Target 元注解说明了注解适用范围是什么
public MyAnnotation {
}Target和Retention一般是必须的.
使用
@Interface就定义了一个注解类型 (或称为元数据 meta-data) , 自动继承了java.lang.annotation.Annotation接口如果在类内声明一个注解 不用加 public
定义自己的注解:
1
2
3
4
5
6
7
8
9
MyAnnotation {
// 以下的无参方法并不真的是方法, 而是注解的属性, default xxx;是该属性的默认值.
// 定义注解的属性 必须有(), 这里的()不是方法的那个括号
String name(); // 没有默认值必须在使用时加参数
int age() default 0; // 如果有默认值, 可以不传入
String[] schools();
}如果参数只有一个, 建议使用value命名, 此时可以省略 "value =", 但是如果用其它的名字就不能省略.
Java-Lesson 11 (Testing)
为什么叫单元测试? -- 这里的"单元"可以理解为Java中某个类的某个方法.
JUnit -- 测试代码放在同一个包的不同目录 (/tests) 下
不同的方法注解
@Test@BeforeAll@BeforeEach@AfterAll@AfterEach@Ignore(暂不执行该测试方法)@Test注解其实是有属性的 (可以加参数)expected期望异常timeout性能测试
举例:
1
2
3
4
5
6
7
8
public void test() throws Exception {
new Math().factorial(-1);
fail("未抛出factorial参数负数异常");
}
quickSort() {...}Java assert -- 不会执行, 除非加
-ea选项.1
2assert false:
"Message saying what happened here";Guava库中的测试方法 --
vertify(true);checkNotNull(s);契约式编程 (DbC) : 客户调用某特定public方法时, 期望产生某些特定的行为
- 可以明确规定这种行为, 就好像合同一样
- 可以通过某些运行时检查来保证这种行为, 也就是前置条件, 后置条件和不变项 (方法开头的, 方法末尾的) .
JUnit5 提供多种动态生成测试的方法, 如
DynamicTest.stream()java 日志 -- SLF4J / Log4j /...
1
2
3
4
5log.trace("");
log.debug("");
log.info("");
log.warn("");
log.error("");为什么使用日志? -- 可以很容易地取消某个级别以下或全部日志, 且容易重新打开和禁止. 可以被定向到不同的处理器, 如控制台, 文件等. 过滤器可以按标准丢弃无用的记录项.
用getLogger创建自己的日志记录器.
1
2
3
4
5
6
7
8
9
10
11// 作为静态字段
private static final Logger logger = Logger.getLogger("com.yourcompany.stack"); // 使用包名
// 自己创建处理器并且调整级别为FINE
static {
logger.setLevel(Level.FINE); // 若此处设置级别为OFF, 即关闭日志
logger.setUseParentHandlers(false);
var handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler);
}然后, 在任何地方插入
logger.fine()或其他的日志.日志的常见用途是记录那些意料之外的异常, 以下两个方法通常使用:
1
void throwing(String className, String methodName, Throwable t)
1
void log(Level l, String message, Throwable t)
例如:
1
2
3var e = new IOException("...") ;
logger.throwing("com.mycompany.Reader", "read", e);
throw e;
日志执行的过程经过了 日志记录器 -> 处理器, 还会有过滤器, 格式化器. 可以根据需要自己编写处理器和过滤器等.
由于所有级别为 INFO / WARNING / SEVERE 的消息都会显示到控制台上, 所以最好之将对用户有意义的消息设置为这几个级别, 而程序员想要的消息设置为FINE级别.
JMH基准测试工具.
JDK 默认附带 VisualVM 分析器. 静态分析器 - Findbugs (->Spotbugs)
要观察类的加载过程, 启动Java虚拟机时使用 -verbose 标志, 这样就可以看到类加载的信息, 有时候, 这对诊断类路径问题很有帮助.
- Xlint 选项告诉编译器找出常见的代码问题.
jconsole工具可以显示有关虚拟机的性能统计的结果, Java任务控制器 (Java Mission Control) 是一个专业级性能分析和诊断工具, 类似jconsole.
Java-Lesson 12 (Collections)
for-in 语法适用于数组和任何Collection对象.
任何自定义的类, 只要实现了
Iterable接口, 都可以用for-in语法.其原理是, Java 5 引入了一个叫
Iterable的接口, 该接口包含一个可以生成Iteraor的方法 -- iterator() 方法. 这个接口就可以用来遍历序列, 所以只要implement了Iterable接口, 就可以用for-in语法遍历了.使用适配器方法 (Adapter Method) 惯用法, 为容器List增加一个能够返回逆序元素迭代器的方法作为适配器方法. 这样就可以实现逆序的 for-in 遍历.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class ReversibleList<T> extends ArrayList<T> {
public Iterable<T> reversed() {
return new Iterable<T>() {
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size() - 1;
public boolean hasNext() {
return current > -1;
}
public T next() {
return get(current--);
}
};
}
};
}
}
ReversibleList<String> strings = new ReversibleList<String>();
for (String s : strings.reversed()) {
//...
}java.util.Collection和java.util.Collections的区别:Collection<E>是一个接口, 继承了Iterable<E>, 是一个由单独元素组成的序列, 其子接口有List<E>Set<E>Queue<E>. (而存放一组键值对用接口Map<K, V>) . 声明的方法有size()add()等.Collections是具体类, 提供了一系列操作或返回集合的静态方法.在
java.util.Collections类中定义了以下的sort方法:1
public static <T extends Comparable<? super T>> void sort(List<T> list)
该方法可以对列表进行排序, 但是要求列表中的所有元素必须实现
Comparable接口 (中的e1.compareTo(e2)方法).如果希望自己编写的类构成的列表可以排序, 需要在该类里实现一个
compareTo方法. 例如:1
2
3
4
5
6
7public class Employee implements Comparable<Employee> {
public int compareTo(Employee o) {
return this.getId().compareTo(o.getId());
}
// ...
}然而
sort方法还有一个重载版本:1
public static <T extends Comparable<? super T>> void sort(List<T> list, Comparator<? super T> c)
这里的参数多了一个比较器
Comparator<T>,Comparator<T>接口是一个函数式接口, 它可以被赋值为lambda表达式或者是一个实现了该接口的对象.这个接口的定义如下:
1
2
3
4
5
public interface Comparator<T> {
int compare(T o1, T o2);
// ...
}所以, 可以写出以下的代码:
1
2
3
4
5
6Comparator<Employee> compareById = new Comparator<Employee> {
public int compare(Employee o1, Employee o2) {
return o1.getId().compareTo(o2.getId());
}
};利用Lambda表达式, 可以写的更优雅些:
1
2Comparator<Employee> compareById = (Employee o1, Employee o2) ->
o1.getId().compareTo( o2.getId() );
Java-Lesson 13 (Inner Class)
jdk.internal.loader.ClassLoaders$AppClassLoader其中,$符号后面的类是$前面的类的内部类, 这是$的含义.
Java-Lesson 14 (Concurrency)
进程: 操作系统进行资源分配和调度的一个独立单位, 也是一个具有独立功能的程序.
线程: 依托于进程而存在, 是CPU调度和分派的基本单位, 它是比进程更小的能独立运行的基本单位. 线程自己基本上不拥有资源, 但是它可与同属一个进程的其他线程共享进程所拥有的全部资源.
区别在于: 进程是资源分配的单位, 而线程是作业调度的单位; 进程拥有自己的地址空间, 而多个线程拥有自己的堆栈和局部变量, 并共享所依托于进程的资源.
Java-Lesson 14.1 (Thread)
Java关于线程编程的抽象:
- Thread对象像是运载火箭, Runnable / Callable的实现对象 (run方法) 像是一个荷载 (payload)
- Runnable / Callable --> 任务; Thread --> 让任务启动.
Runnable接口 -- 一个函数式接口, 里面只有一个run方法.实现
Runnable接口实例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class LiftOff implements Runnable {
protected int countDown = 10; // default
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while (countDown-- > 0) {
System.out.println(status());
Thread.yield();
}
}
}1
2
3
4
5
6public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff(10);
launch.run();
}
}Thread类, 表示线程, 如果要创建一个新线程, 就要新创建一个Thread.1
2
3
4
5
6
7
8
9public class BasicThreads {
public static void main(String[] args) {
// 把任务装进线程
Thread t = new Thread(new LiffOff(10));
t.start();
// start 之后, 就会进入新线程的run()方法.
System.out.println("Waiting for LiftOff");
}
}除了通过实现
Runnable接口来实现新的线程之外, 还可以直接继承Thread类, 直接重写Thread类下的run方法即可.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
super(Integer.toString(++threadCount));
start();
}
public String toString() {
return "#" + getName() + "(" + countDown + "), ";
}
public void run() {
while (true) {
System.out.print(this);
if (--countDown == 0) {
return;
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SimpleThread();
}
}
}这5个线程创建完之后, 线程运行的顺序不是你可以控制的, 多线程并发时, 执行的先后关系是不确定的, 这要交由操作系统的调度器决定.
创建线程并启动 (start) 后, 线程并不会立即执行, 而只是通知了JVM这个线程可以开始运行了. 然而调度过程不是由你控制的, 所以你不应该显式地调用
run方法, 而是调用start方法. (run不是你能控制的)尽管你知道你的机器是4核的, 并且写Java程序运行了4个线程, 但是这并不意味着就只有这4个线程, JVM底层还有线程, 所以, 一般我们不自己创建和销毁单个线程, 而是利用
ExecuteService.一般我们创建线程之后, 不会自己做启动这件事, 可以使用
ExecutorService启动.1
2
3
4
5
6
7
8
9
10
11public class CachedThreadPool {
public static void main(String[] args) {
// 这条语句会创建一个带缓存的线程池
ExecutorService exec = Executors.newCachedThreadPool(); // 新的带缓存的线程池
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff(10));
}
// 关闭线程池, 不再接受新任务, 不是把虚拟机停掉, 只是不再给出任务
exec.shutdown();
}
}带缓存的线程池 -- 根据需要创建新线程的线程池, 如果现有线程没有可用的, 则创建一个新线程并添加到池中, 如果有被使用完但是还没被销毁的线程, 就复用线程池.
每一个线程要能够执行, JVM会为之准备一堆的事情 (准备栈, PC, ...), 严重地消耗时间和内存空间. 所以为了提高效率, 应该避免手动创建和销毁线程, 而是交给线程池管理.
还可以创建一个固定线程数的线程池 (FixedThreadPool), 在任何时候最多只有n个线程被创建, 若在所有的线程都处于活动状态时, 有其他任务提交, 他们将等待队列中直到线程可用.
1
2
3
4
5ExecutorService exec = Executors.newFixedThreadPool(5); // 最多只有5个线程被创建
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff(10));
}
exec.shutdown();线程池常见用法:
调用
Executor类的静态方法 新建带缓存的或者固定线程数的线程池.调用
submit提交Runnable或Callable对象.保存好返回的
Future对象, 以便得到结果或者取消任务.当不想再提交任何任务时, 调用shutdown. (-> 不是销毁掉, 而是进入不接受新任务的状态)
如果需要获得一个返回值, 而不是像 run 方法一样返回void, 那么就使用
Callable<V>对象, 而不是Runnable<V>对象.举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class MyCallable implements Callable<String> {
public String call() throws Exception {
System.out.println("做一些耗时的任务...");
Thread.sleep(5000);
return "OK";
}
}
public class FutureSimpleDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newCachedThreadPool();
// 这里使用的是submit, 而不是execute, 区别在于submit会返回一个Future类型的对象
Future<String> future = executorService.submit(new MyCallable());
System.out.println("do something...");
System.out.println("得到异步任务返回结果: " + future.get());
System.out.println("Completed");
}
}什么是
Future类型的对象?1
public interface Future<V>
Future代表了一个异步计算的结果. 这个结果只有等到计算过程结束后, 才能通过
get()方法获得, 如果还没有结束, get方法的调用就会阻塞 (block) .Thread.sleep(1000);线程停止1000ms, 为了避免歧义, 现在通常使用TimeUnit.MILLISECONDS.sleep(100);实际上使CPU处于了不是满载, 但是是停止状态, CPU在空转 (而不是在进行任何操作) , 等待若干时钟周期, 然后进入下一条指令执行.yield让位. 提醒调度器当前线程愿意让出它当前对于处理器的使用, 由调度器决定是否要将此线程让出.yield和sleep的主要区别:sleep是暂停CPU的处理下一条指令.
- yield会临时暂停当前线程, 让同样优先级的正在等待的线程有机会执行
- 若没有正在等待的线程或者所有正在等待的线程的优先级都较低, 则继续运行
- 执行yield的线程何时继续运行由线程调度器来决定, 不同厂商可能有不同行为
- yield方法不保证当前的线程会暂停或停止, 但是可以保证当前线程在调用yield方法时会放弃CPU (让调度器决策一下)
优先级 (priority) : 可以使用
setPriority()方法来改变线程的优先级 (Thread.MAX_PRIORITY) , 但是不应该自己改变优先级, 而应该让系统自己决定.Daemon线程, demon线程是一种不会阻止JVM退出 (JVM的main方法运行结束了) 的线程. 即main thread执行完了且其它所有的非daemon threads都执行完了, JVM就会退出.
Java-Lesson 14.2 (Synchronize)
本节讲解线程之间的同步问题.
Thread类有一个join()方法, join方法直到run方法停止后才会执行, 若run方法运行被打断, 抛出异常.利用 join() 可以保证一个线程在另一个线程之后执行. (实现线程同步)
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class Sleeper extends Thread {
private int duration;
public Sleeper(String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}
public void run() {
try {
sleep(duration);
} catch (InterruptedException e) {
System.out.println(getName() + " was interrupted.");
return;
}
System.out.println(getName() + " has awakened");
}
}
class Joiner extends Thread {
private Sleeper sleeper;
public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
public void run() {
try {
sleeper.join(); // 必须sleeper线程运行完之后, join()方法才执行, joiner线程才能往下执行
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}Thread类里面还有一个interrupt()方法, 给当前线程发一个interrupt消息打断这个线程, 如果线程在sleep中收到了interrupt消息, 就会触发一个InterruptedException.每个线程运行时都可能发生异常 (这种运行时异常编译时往往无法捕获), 而这些异常显然无法通过在main thread里面写 try catch来捕获, 而是应该使用UncaughtExceptionHandler, 即创建一个异常处理器, 实现
Thread.UncaughtExceptionHandler接口, 然后对 (希望创建的) 线程t调用方法t.setUncaughtExceptionHandler(new ...)来设置异常处理器 (表示如果线程t中出现异常, 希望被怎么处理), 设置了Uncaught异常处理器后线程t中抛出的异常就会被异常处理器处理 (从而没有直接抛出来, 而是被handler解决).更进一步地, 当我们使用很多线程时, 我们希望对于这些所有线程, 对于整个JVM设置一个default的 UncaughtExceptionHandler. 这样在整个JVM运行中, 只要出现了运行时异常, 就会被默认Handler处理, 这是一个简单的做法.
1
2
3
4
5
6
7
8public class SettingDefaultHandler {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}资源共享问题
在数据库领域, 这个问题就是数据一致性.
例如: 多个线程同时操作同一个数据 (如局部变量), 由于堆区的数据是共享的, 所以数据不一致.
race condition(竞争条件) 多个资源竞争不能被同时访问的资源
解决方案: 对资源加锁, 确保一个时刻只有一个任务在使用共享资源 (使其互斥)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class MutexEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
private Lock lock = new ReentrantLock(); // 即创建一个锁
public int next() {
// 加锁
lock.lock();
try {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
} finally { // 即是return了, finally里面的语句还会执行
lock.unlock(); // 一定要用try-catch的finally释放锁
}
}
public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator(), 10);
}
}ReentrantLock类, 里面的方法tryLock()尝试获取锁, 返回成功与否, 若有参数tryLock(timeout, TimeUnit.SECONDS), 则会等timeout个TimeUnit的时间, 若这段时间内获取锁成功也返回true.Synchronized关键字 -- 只允许一个线程进入这一方法, 这整个方法不允许被多个线程同时调用.1
public synchronized int next() {}
synchronized标记了是一个 临界区 (critical sections) , 整个临界区一次只有一个线程可以进入.synchronized不仅可以用于修饰方法, 也可以修饰代码片段, 如:1
2
3
4
5
6
7
8
9
10class A {
public void f() {
int field;
synchronized (this) {
p.incrementX();
p.incrementX();
}
}
}为什么要加
this? 这涉及了Javasynchronized的设计理念,synchronized实际上是对于对象加锁.Java VM中为每一个对象都对应维护一个 monitor (管程) , 用于实现多个线程执行该对象上同步方法时JVM检查该对象的管程:
- 如果该对象管程未被占有, 当前调用线程可获得所有权并被允许执行该方法;
- 如果一个管程被另一个线程所有, 则调用线程需要等待管程被释放.
当一个方法完成同步方法调用时, 它释放管程所有权, 等待该管程的线程被允许执行同步方法.
而synchronized的实现就是通过控制管程 (monitor) 的所有权做到的.
即, synchronized的方法的调用必须要获得该方法所在对象的管程所有权.
生产者-消费者问题, 对于一个对象的
get和put方法, 给二者加上synchronized修饰, 那么在执行put / get时, 方法所在对象的管程被占有, 保证另一个方法不会同时执行.如果buffer满了, 那么等待直到消费者来get.
1
2
3
4
5
6
7
8
9public synchronized void Put(char c) {
while (count == buffer.length) {
try { wait(); }
catch (InterruptedException e) { }
finally { }
}
// producing...
notify();
}wait和notify是什么?wait-- 释放锁 (管程), 停下此线程 (suspend the calling thread), 等待 (希望别人拿走管程) (release the ownership of the monitor),notify-- 如果一个线程执行了wait()方法而被挂起, 那么只有当另一个线程调用了notify() / notifyAll()睡着的线程才会醒来.简单来说,
notify就是提醒睡着的线程醒来 (通知所有正在等待获取monitor所有权的线程醒来).wait的作用是: 如果拿到了锁, 但是检查条件后发现不满足使用条件(不应该做后续事情), 那么不要拿着锁不放, 否则就死锁了. 通过wait方法把锁让出来, 使得别的线程可以调用synchronized方法. (在拿到锁后不应该做事时不要拿着锁死等不放)wait()和notify()都是Object类下定义的方法, 因为管程是每一个对象所维护的, 而这两个方法用于释放和转移monitor的所有权, 所以这两个方法在Object下定义, 而不是在Concurrency包下定义.举例: 生产者消费者 (
PC.java)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74class Buffer {
private char[] buffer;
private int count = 0, in = 0, out = 0;
Buffer(int size) {
buffer = new char[size];
}
public synchronized void Put(char c) {
while (count == buffer.length) {
try { wait(); }
catch (InterruptedException e) { }
finally { }
}
System.out.println("Producing " + c + " ...");
buffer[in] = c;
in = (in + 1) % buffer.length;
count++;
notify();
}
public synchronized char Get() {
while (count == 0) {
try { wait(); }
catch (InterruptedException e) { }
finally { }
}
char c = buffer[out];
out = (out + 1) % buffer.length;
count--;
System.out.println("Consuming " + c + " ...");
notify();
return c;
}
}
class Producer extends Thread {
private Buffer buffer;
Producer(Buffer b) {
buffer = b;
}
public void run() {
for (int i = 0; i < 10; i++) {
buffer.Put((char) ('A' + i % 26));
}
}
}
class Consumer extends Thread {
private Buffer buffer;
Consumer(Buffer b) {
buffer = b;
}
public void run() {
for (int i = 0; i < 10; i++) {
buffer.Get();
}
}
}
public class PC {
public static void main(String[] args) {
Buffer b = new Buffer(4);
Producer p = new Producer(b);
Consumer c = new Consumer(b);
p.start();
c.start();
}
}线程状态: Start ; Runnable (等待运行状态, 可以由Running状态yield退回) ; Running ; Blocked(由Running状态
sleep/suspend/wait进入,resume/notify退出) ; Stop (线程执行停止)wait和sleep的区别:- 调用wait方法时, 线程在等待时会释放掉它所获得的monitor, sleep则不会;
- wait应该在同步代码块中调用, 而sleep可以在任何地方调用;
- Thread.sleep()是一个静态方法, 作用在当前线程上,
而wait是一个实例方法,
只能在其他线程调用本实例的
notify()方法时被唤醒.
只是要让线程停止一段时间 -- sleep() ; 要实现线程之间通信 -- wait()
线程本地存储 (Thread local Storage, TLS) : 变量的可见域为线程内. (在线程内全局可访问, 但是不能被其他线程访问到, 这样保证了数据的独立性, 从而避免了线程同步问题)
Java使用
ThreadLocal对象实现线程本地存储:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44class Accessor implements Runnable {
private final int id;
public Accessor(int idn) {
id = idn;
}
public void run() {
while (!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment(); // 尽管是同一个静态字段, 但是由于引用者处于不同线程, 所以各自的线程拿到的和使用的数据是各自的
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ": " + ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() { // 定义线程局部变量
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() {
return value.get();
}
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new Accessor(i));
TimeUnit.SECONDS.sleep(3); // Run for a while
exec.shutdownNow(); // All Accessors will quit
}
}
Java-Lesson 14.3 (concurrent包)
Java中的
java.util.concurrent包下有很多有用的工具.CountDownLatch允许一个或多个线程等待直到一系列操作在另外的线程中完成. (用于线程同步, 比join更灵活)1
2
3
4CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
startSignal.countDown(); // let all threads proceed //对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
doneSignal.await(); // wait for all to finish //阻塞当前线程,将当前线程加入阻塞队列。CyclicBarrier循环栅栏 : 只有当所有的栅栏拦住的所有线程都准备好时, 栅栏才放开.1
2CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM, new AggregatorThread()); // 参数分别是 1:参与线程的个数 2: 线程到达屏障时, 优先执行的action
cyclicBarrier.await(); // 表示线程已经到达栅栏ScheduledThreadPoolExecutor一种特殊的ThreadPoolExecutor(线程池执行者) , 比起Cached和Fixed的Executor,ScheduledThreadPoolExecutor可以让任务等一段时间在运行, 也可以规定多个任务以一个固定的速率启动. (can additionally schedule commands to run after a given delay, or to execute periodically.)1
final ScheduledThreadPoolExecutor scheduler = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
Semaphore信号量: 指定一个资源使用量的上限是多少. (若信号量为1, 即是互斥的资源)CompletableFutureExchangerDelayQueue放入数据设置delay时间, 只有过了指定的时间之后数据才能被取走. (用于服务器对于用户请求排队)
Java-Lesson 15 (VM)
必读: https://blog.jamesdbloom.com/JVMInternals.html
参考: https://zhuanlan.zhihu.com/p/682004204
Java虚拟机的内存包括以下5个区域 :
堆 (Heap) -- new出来的对象, 可以被所有线程共享
栈 (JVM Language Stacks) -- 存放基本变量类型和引用类型
方法区 (Method Area) -- 包含了所有的class和static变量, 包含类的数据(静态变量, 静态方法, 常量池, 代码...), 被所有线程所共享 ( (HotSpot中) 方法区仅仅在逻辑上独立, 物理上包含在Java堆中 )
PC Registers
Native Method Stacks (运行用C语言等编写的本地方法时使用)
方法区中保存了每一个类的信息. 包括:
- Classloader Reference 由哪个类加载器加载的
- Run Time Constant Pool 运行时常量池 (类似C/ C++的符号表)
- Field data
- Method data
- Method code
使用
javap命令查看字节码信息. 敲入javap -help以获取帮助.1
javap -v -p -s -sysinfo -constants MyClass
获取字节码相关信息.
所有的类都是在对其第一次使用 (类缓存保证了只加载第一次) 时, 动态加载到JVM中去的.
一旦某个类的
Class对象被载入内存, 它就被用来创建这个类的所有对象.当程序创建第一个对类的静态成员的引用时 (包括使用new语句新建对象) , JVM会使用类加载器根据类名查找.class文件.
类的加载 (Load) -> 类的链接 (Link) -> 类的初始化 (Initialize)
加载: 将class文件字节码内容加载到内存中, 并将这些静态数据转换成方法区的运行时数据结构, 然后生成一个代表这个类的java.lang.Class对象 ( Class对象在堆区 ).
Java类由
java.lang.ClassLoader加载, 那么ClassLoader类由谁加载? --Bootstrap Class Loader(/ˈbutˌstræp/) , 特殊的Class Loader, 一般不用Java语言编写, 而是用C语言等本地代码写, 与具体的平台实现相关.此外, 还有extension class loader (java 9以后叫platform class loader), 用于加载放在
java.ext.dirs目录下的java扩展类型.system class loader / application class loader, 加载你自己在java应用里面写的类型, 从命令行选项中的
-classpath或-cp下寻找.链接: 将Java类的二进制代码合并到JVM的运行状态之中的过程.
验证 -- 验证加载的类信息没有安全方面的问题,
准备 -- 在方法区为static变量分配内存并设置默认初始值,
解析-- 常量池内的符号引用(name) 被替换为 直接引用(地址).
初始化:
执行类构造器中的<clinit>() 方法, 由所有static变量的赋值和静态代码块中的语句合并起来构成. (按源码的字面顺序执行静态语句)
若发现其父类没有初始化, 先触发其父类的初始化
保证一个类的<clinit>() 方法在多线程环境中被正确加锁和同步.
什么时候会发生类的初始化?
类的主动引用 (一定发生类的初始化)
1. 虚拟机启动时, 先初始化main方法所在的类
2. new一个对象时
3. 调用类的静态成员 (除final常量) 和静态方法
4. 使用
java.lang.reflect包的方法对类进行反射调用 5. 初始化一个类, 但是父类没有被初始化则会先初始化其父类
类的被动引用 (不会发生类的初始化)
- 当访问一个静态域时, 只有真正声明这个域的类才会被初始化. 如: 当通过子类引用父类的静态变量, 不会导致子类初始化.
- 通过数组定义类引用, 不会触发此类的初始化. (数组只是给它定义一个名字)
(
A[] arr = new A[5];) - 引用常量不会触发此类的初始化. (常量在链接时就被放入常量池了)
初始化的顺序: 静态字段和静态代码块初始化 -> 成员变量和非静态代码块初始化 -> 构造方法; 若有父类, 先初始化父类.
类缓存 -- 标准的JavaSE类加载器可以按照要求查找类, 但一旦某个类被加载到类加载器中, 它将维持一段时间 (缓存), 不过JVM回收机制可以回收这些Class对象.
类加载器 -- 用来把类 (class) 装载进内存. 有引导类加载器, 扩展类加载器, 系统类加载器 (AppClassLoader) (最常用, 用于加载用户自己写的类)
用户也可以自定义类加载器, 是Application Class Loader的子类. (重写类加载器类的
findClass(String name)方法即可实现自己的类加载器) 实现自己的类加载器是有用的, 例如浏览器使用独立的类加载器加载来自不同网页的小程序, 这样即使小程序重名也会被认为成是不同的部分.双亲委派机制 -- 类加载时, 类加载器会一直向父加载器 (parent class loader) 委派 (application class loader -> platform class loader) , 直到根类加载器 (引导类加载器, bootstrap class loader), 如果父类加载器加载失败 (返回null) , 就依次交由下一级进行加载. (所以从库里找的顺序是从父到子.)
父类 (parent) 加载器并不是子类加载器的父类 (super class).
这一机制的核心思想在于, 当遇到一个加载请求时, 先尝试的是向上委派, 而不是先尝试自己加载. 下层加载的类型不能被上层看到.
所以你不能写个恶意的String类型替换掉系统内部的String.
类加载机制的高级应用: 对加载的字节码进行修改, 改变类的行为; 运行时动态创建类型 -> 高级的Java应用技术.
Java类加载机制: 当触发类加载的时候,类加载器也不是直接加载这个类。首先交给
AppClassLoader,它会查看自己有没有加载过这个类,如果有直接拿出来,无须再次加载,如果没有就将加载任务传递给ExtClassLoader,而ExtClassLoader也会先检查自己有没有加载过,没有又会将任务传递给BootstrapClassLoader,最后BootstrapClassLoader会检查自己有没有加载过这个类,如果没有就会去自己要寻找的区域去寻找这个类,如果找不到又将任务传递给ExtClassLoader,以此类推最后才是AppClassLoader加载我们的类。
Java Regexes
Java也有正则表达式, 常用于各种字符串处理.
.任意字符 (不能是换行)*重复0次或更多次+重复1次或更多次?重复0或1次 (表示可选的){m}重复m次{m, n}重复m ~ n次^匹配字符串开头$匹配字符串结尾[]必须匹配括号里的内容[^]必须匹配除了括号里以外的字符 (这里^的意思与之前)\w: [a-zA-Z0-9_] 即构成一个单词的所有可能组合, 字母数字下划线.\d: [0-9] digits\D: [^0-9] not a digit\s: 空白字符\S: 空白字符\W: [^a-zA-Z0-9_] 非单词字符以外所有字符
Jar包
只要涉及到Java的项目, 就不可避免地接触到jar包. 而实际开发中,maven, gradle等项目管理工具为我们自动地管理jar包以及相关的依赖, 让jar包的调用看起来如黑盒一般"密不透风". 本节让我们打开这个黑盒, 了解有关jar包的知识. (参考 - Java核心技术)
jar包就是 Java Archive File, 是Java的一种文档格式, 是一种与平台无关的文件格式, 可将多个文件合成一个文件. 与zip包很相似, 准确的说, jar和zip的唯一区别就是在jar文件的内容中, 包含了一个
META-INF/MAINIFEST.MF文件, 作为jar里面的详情单.jar包主要是对class文件进行打包, 这意味着jar包是跨平台的.
使用指令
jar -tf xxx.jar查看jar里面的内容.可以查看到jar包里面一般有 .MF .class 文件, 还有静态资源文件如 .html, .css 以及 .js等.
jar包用于发布, 方便将自己实现的功能提供给别人使用.
还有war包, war包是一个可以直接运行的web模块, 通常应用于web项目中, war包可以部署到Tomcat等容器中, war包能打包的内容jar包都可以打包.
如何打jar包? 首先将编写好的java项目用
javac命令生成字节码文件, 然后在命令行中执行1
jar -cvf xxx.jar com/src/A.class com/B.class
c表示要创建一个新的jar包,v表示创建的过程中在控制台输出创建过程的一些信息,f表示给生成的jar包命名.
手动打jar包需要自己新建MANIFEST文件, 一般使用intellij idea打包, 步骤如下:
- 找到FILE/ Project Structure
- 点击 Artifacts
- 点击绿色的 "+" 号, 选择JAR -> Empty
- 然后按照提示设置jar包名, 目标路径, 需要打包的文件即可.
- 然后点击菜单中的Build, 选择Build Artifacts..., 然后双击弹窗中的Build即可. (END)
如何执行一个jar包? 很简单, 一般只要执行指令:
1
java -jar xxx.jar
但是, 这样可能会出现不知道指定的main方法所在的问题, 可以通过一下指令动态指定:
1
java -cp xxx.jar com.src.A
即通过-cp指定main方法所在的类.
甚至用户可以通过双击JAR文件图标来启动应用程序.
读取jar包内的资源文件, 使用
getResourceAsStream()这一api来实现.如何使用IDEA导入第三方jar包: 使用IDEA打开项目, 找到FILE/ Project Structure , 然后选择 Modules -> Dependencies -> + -> Jars or directories, 导入jar包所在的本地路径.