Qeuroal's Blog

静幽正治

字符串和编码

String

Java 中,String 是一个引用类型,它本身也是一个 class。但是,Java 编译器对 String 有特殊处理,即可以直接用 “…” 来表示一个字符串:

1
String s1 = "Hello!";

实际上字符串在 String 内部是通过一个 char[] 数组表示的,因此,按下面的写法也是可以的:
1
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});

因为 String 太常用了,所以 Java 提供了 "..." 这种字符串字面量表示方法。

Java 字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的 private final char[] 字段,以及没有任何修改 char[] 的方法实现的。看下例:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String s = "Hello";
System.out.println(s);
s = s.toUpperCase();
System.out.println(s);
}
}

输出

1
2
Hello
HELLO

个人认为:字符串内容没有改变,因为字符串是引用类型,而 s.toUpperCase() 是创建了一个新的字符串,再用 s 指向它。只是原来的字符串无法通过 s 访问罢了。

字符串比较

当我们想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用 equals() 方法而不能用 ==
看下例:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}

输出
1
2
true
true

从表面上看,两个字符串用 ==equals() 比较都为 true,但实际上那只是 Java 编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然 s1s2 的引用就是相同的。
所以,这种 == 比较返回 true 纯属巧合。换一种写法,== 比较就会失败:
1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase(); // 因为在编译期间,只有 hello 和 HELLO 两个字符串
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}

输出
1
2
false
true

结论:两个字符串比较,必须总是使用 equals() 方法。要忽略大小写比较,使用 equalsIgnoreCase() 方法。String 类还提供了多种方法来搜索子串、提取子串。常用的方法有:

子串、提取子串

  1. 是否包含子串

    1
    2
    // 是否包含子串:
    "Hello".contains("ll"); // true

    注意到 contains() 方法的参数是 CharSequence 而不是 String,因为 CharSequenceString 的父类。

  2. 搜索子串的更多的例子:

    1
    2
    3
    4
    "Hello".indexOf("l"); // 2
    "Hello".lastIndexOf("l"); // 3
    "Hello".startsWith("He"); // true
    "Hello".endsWith("lo"); // true
  3. 提取子串

    1
    2
    "Hello".substring(2); // "llo"
    "Hello".substring(2, 4); // "ll"

注意索引号是从 0 开始的

去除首尾空白字符

使用 trim() 方法可以移除字符串首尾空白字符。空白字符包括 空格\t\r\n

1
"  \tHello\r\n ".trim(); // "Hello"

注意trim() 并没有改变字符串的内容,而是返回了一个新字符串。
另一个 strip() 方法也可以移除字符串首尾空白字符。它和 trim() 不同的是,类似中文的空格字符 \u3000 也会被移除:

1
2
3
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"

String 还提供了 isEmpty()isBlank() 来判断字符串是否为空和空白字符串:
1
2
3
4
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符

替换子串

要在字符串中替换子串,有两种方法。一种是根据字符或字符串替换:

1
2
3
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~

另一种是通过正则表达式替换:
1
2
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"

分割字符串

要分割字符串,使用 split() 方法,并且传入的也是正则表达式:

1
2
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}

拼接字符串

拼接字符串使用静态方法 join(),它用指定的字符串连接字符串数组:

1
2
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"

格式化字符串

字符串提供了 formatted() 方法和 format() 静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
}
}

有几个占位符,后面就传入几个参数。参数类型要和占位符一致。我们经常用这个方法来格式化信息。常用的占位符有:
- %s:显示字符串;
- %d:显示整数;
- %x:显示十六进制整数;
- %f:显示浮点数。
占位符还可以带格式,例如 %.2f 表示显示两位小数。如果你不确定用啥占位符,那就始终用 %s,因为 %s 可以显示任何数据类型。要查看完整的格式化语法,请参考 JDK文档

类型转换

要把任意基本类型或引用类型转换为字符串,可以使用静态方法 valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法:

1
2
3
4
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c

要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为 int 类型:
1
2
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255

把字符串转换为 boolean 类型:
1
2
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false

要特别注意,Integer 有个 getInteger(String) 方法,它不是将字符串转换为 int,而是把该字符串对应的系统变量转换为 Integer
1
Integer.getInteger("java.version"); // 版本号,11

转换为char[]

Stringchar[] 类型可以互相转换,方法是:

1
2
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String

如果修改了 char[] 数组,String 并不会改变:
1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s);
cs[0] = 'X';
System.out.println(s);
}
}

这是因为通过 new String(char[]) 创建新的 String 实例时,它并不会直接引用传入的 char[] 数组,而是会复制一份,所以,修改外部的 char[] 数组不会影响 String 实例内部的 char[] 数组,因为这是两个不同的数组。
String 的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。

字符编码

StringBuilder

Java 编译器对 String 做了特殊处理,使得我们可以直接用 + 拼接字符串。

1
2
3
4
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}

虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响 GC 效率。
为了能高效拼接字符串,Java 标准库提供了 StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往 StringBuilder 中新增字符时,不会创建新的临时对象:
1
2
3
4
5
6
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();

StringBuilder 还可以进行链式操作:
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder(1024);
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString());
}
}

如果我们查看 StringBuilder 的源码,可以发现,进行链式操作的关键是,定义的 append() 方法会返回 this,这样,就可以不断调用自身的其他方法。

仿照 StringBuilder,我们也可以设计支持链式操作的类。例如,一个可以不断增加的计数器:

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
public class Main {
public static void main(String[] args) {
Adder adder = new Adder();
adder.add(3)
.add(5)
.inc()
.add(10);
System.out.println(adder.value());
}
}

class Adder {
private int sum = 0;

public Adder add(int n) {
sum += n;
return this;
}

public Adder inc() {
sum ++;
return this;
}

public int value() {
return sum;
}
}

注意:对于普通的字符串 + 操作,并不需要我们将其改写为 StringBuilder,因为 Java 编译器在编译时就自动把多个连续的 + 操作编码为 StringConcatFactory 的操作。在运行期,StringConcatFactory 会自动把字符串连接操作优化为数组复制或者 StringBuilder 操作。

你可能还听说过 StringBuffer,这是 Java 早期的一个 StringBuilder 的线程安全版本,它通过同步来保证多个线程操作 StringBuffer 也是安全的,但是同步会带来执行速度的下降。

StringBuilderStringBuffer 接口完全相同,现在完全没有必要使用 StringBuffer

StringJoiner

要高效拼接字符串,应该使用 StringBuilder
很多时候,我们拼接的字符串像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sb = new StringBuilder();
sb.append("Hello ");
for (String name : names) {
sb.append(name).append(", ");
}
// 注意去掉最后的", ":
sb.delete(sb.length() - 2, sb.length());
sb.append("!");
System.out.println(sb.toString());
}
}

类似用分隔符拼接数组的需求很常见,所以 Java 标准库还提供了一个 StringJoiner 来干这个事:
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.StringJoiner;

public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}

慢着!用 StringJoiner 的结果少了前面的 "Hello" 和结尾的 "!" 遇到这种情况,需要给 StringJoiner 指定 “开头” 和“结尾”:
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}

String.join()

String 还提供了一个静态方法 join(),这个方法在内部使用了 StringJoiner 来拼接字符串,在不需要指定 “开头” 和“结尾”的时候,用 String.join() 更方便:

1
2
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);

包装类型

我们已经知道,Java的数据类型分两种:

  • 基本类型:byteshortintlongbooleanfloatdoublechar
  • 引用类型:所有 classinterface 类型

引用类型可以赋值为 null,表示空,但基本类型不能赋值为 null

1
2
String s = null;
int n = null; // compile error!

那么,如何把一个基本类型视为对象(引用类型)?

比如,想要把 int 基本类型变成一个引用类型,我们可以定义一个 Integer 类,它只包含一个实例字段 int,这样,Integer 类就可以视为 int 的包装类(Wrapper Class)

1
2
3
4
5
6
7
8
9
10
11
public class Integer {
private int value;

public Integer(int value) {
this.value = value;
}

public int intValue() {
return this.value;
}
}

定义好了 Integer 类,我们就可以把 intInteger 互相转换:
1
2
3
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();

实际上,因为包装类型非常有用,Java 核心库为每种基本类型都提供了对应的包装类型:

基本类型 对应的引用类型
boolean java.lang.Boolean
byte java.lang.Byte
short java.lang.Short
int java.lang.Integer
long java.lang.Long
float java.lang.Float
double java.lang.Double
char java.lang.Character
我们可以直接使用,并不需要自己去定义:
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例:
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
}
}

Auto Boxing

因为int和Integer可以互相转换:

1
2
3
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();

所以,Java 编译器可以帮助我们自动在 intInteger 之间转型:
1
2
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()

这种直接把 int 变为 Integer 的赋值写法,称为自动装箱(Auto Boxing),反过来,把 Integer 变为 int 的赋值写法,称为自动拆箱(Auto Unboxing)。

注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。

装箱和拆箱会影响代码的执行效率,因为编译后的 class 代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报 NullPointerException

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Integer n = null;
int i = n;
}
}

不变类

所有的包装类型都是不变类。我们查看 Integer 的源码可知,它的核心代码如下:

1
2
3
public final class Integer {
private final int value;
}

因此,一旦创建了 Integer 对象,该对象就是不变的。
对两个 Integer 实例进行比较要特别注意:绝对不能用 == 比较,因为 Integer 是引用类型,必须使用 equals() 比较:
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;
System.out.println("x == y: " + (x==y)); // true
System.out.println("m == n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
}
}

仔细观察结果的童鞋可以发现,== 比较,较小的两个相同的 Integer 返回 true,较大的两个相同的 Integer 返回 false,这是因为 Integer 是不变类,编译器把 Integer x = 127; 自动变为 Integer x = Integer.valueOf(127);,为了节省内存,Integer.valueOf()对于较小的数,始终返回相同的实例,因此,== 比较 “恰好” 为 true,但我们绝不能因为 Java 标准库的 Integer 内部有缓存优化就用 == 比较,必须用 equals()方法比较两个 Integer。

按照语义编程,而不是针对特定的底层实现去“优化”。

因为 Integer.valueOf() 可能始终返回同一个 Integer 实例,因此,在我们自己创建 Integer 的时候,以下两种方法:

  • 方法 1:Integer n = new Integer(100);
  • 方法 2:Integer n = Integer.valueOf(100);

方法 2 更好,因为方法 1 总是创建新的 Integer 实例,方法 2 把内部优化留给 Integer 的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。

我们把能创建 “新” 对象的静态方法称为静态工厂方法。Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。

创建新对象时,优先选用静态工厂方法而不是 new 操作符

如果我们考察 Byte.valueOf() 方法的源码,可以看到,标准库返回的 Byte 实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。

进制转换

Integer 类本身还提供了大量方法,例如,最常用的静态方法 parseInt() 可以把字符串解析成一个整数:

1
2
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析

Integer还可以把整数格式化为指定进制的字符串:
1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
}
}

注意:上述方法的输出都是String,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100在内存中总是以4字节的二进制表示:

1
00000000 00000000 00000000 01100100 

我们经常使用的 System.out.println(n); 是依靠核心库自动把整数格式化为 10 进制输出并显示在屏幕上,使用 Integer.toHexString(n) 则通过核心库自动把整数格式化为 16 进制。

这里我们注意到程序设计的一个重要原则:数据的存储和显示要分离

Java 的包装类型还定义了一些有用的静态变量

1
2
3
4
5
6
7
8
9
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)

最后,所有的整数和浮点数的包装类型都继承自 Number,因此,可以非常方便地直接通过包装类型获取各种基本类型:
1
2
3
4
5
6
7
8
// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();

处理无符号整型

在 Java 中,并没有无符号整型(Unsigned)的基本数据类型。byteshortintlong 都是带符号整型,最高位是符号位。而 C 语言则提供了 CPU 支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在 Java 中就需要借助包装类型的静态方法完成。

例如,byte 是有符号整型,范围是 -128~+127,但如果把 byte 看作无符号整型,它的范围就是 0~255。我们把一个负的 byte 按无符号整型转换为 int

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}

因为 byte 的 -1 的二进制表示是 11111111,以无符号整型转换后的 int 就是 255。

类似的,可以把一个 shortunsigned 转换为 int,把一个 intunsigned 转换为 long

小结

  • Java 核心库提供的包装类型可以把基本类型包装为 class
  • 自动装箱和自动拆箱都是在编译期完成的(JDK>=1.5)
  • 装箱和拆箱会影响执行效率,且拆箱时可能发生 NullPointerException
  • 包装类型的比较必须使用 equals()
  • 整数和浮点数的包装类型都继承自 Number
  • 包装类型提供了大量实用方法。

JavaBean

在 Java 中,有很多 class 的定义都符合这样的规范:

  • 若干 private 实例字段;
  • 通过 public 方法来读写实例字段。

例如:

1
2
3
4
5
6
7
8
9
10
public class Person {
private String name;
private int age;

public String getName() { return this.name; }
public void setName(String name) { this.name = name; }

public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
}

如果读写方法符合以下这种命名规范:
1
2
3
4
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

那么这种 class 被称为 JavaBean
上面的字段是 xyz,那么读写方法名分别以 get 和 set 开头,并且后接大写字母开头的字段名 Xyz,因此两个读写方法名分别是 getXyz() 和 setXyz()。
boolean 字段比较特殊,它的读方法一般命名为 isXyz():
1
2
3
4
// 读方法:
public boolean isChild()
// 写方法:
public void setChild(boolean value)

我们通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。例如,name属性

  • 对应的读方法是String getName()
  • 对应的写方法是setName(String)

只有getter的属性称为只读属性(read-only),例如,定义一个age只读属性:

  • 对应的读方法是int getAge()
  • 无对应的写方法setAge(int)

类似的,只有setter的属性称为只写属性(write-only)。
很明显,只读属性很常见,只写属性不常见。
属性只需要定义getter和setter方法,不一定需要对应的字段。例如,child只读属性定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
private String name;
private int age;

public String getName() { return this.name; }
public void setName(String name) { this.name = name; }

public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }

public boolean isChild() {
return age <= 6;
}
}

枚举 JavaBean 属性

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
public class Main {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}
}
}

class Person {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
public boolean isChild() {
return age < 6;
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
age
public int Person.getAge()
public void Person.setAge(int)
child
public boolean Person.isChild()
null
class
public final native java.lang.Class java.lang.Object.getClass()
null
name
public java.lang.String Person.getName()
public void Person.setName(java.lang.String)

枚举类

在Java中,我们可以通过static final来定义常量。例如,我们希望定义周一到周日这7个常量,可以用7个不同的int表示:

1
2
3
4
5
6
7
8
9
public class Weekday {
public static final int SUN = 0;
public static final int MON = 1;
public static final int TUE = 2;
public static final int WED = 3;
public static final int THU = 4;
public static final int FRI = 5;
public static final int SAT = 6;
}

使用常量的时候,可以这么引用:
1
2
3
if (day == Weekday.SAT || day == Weekday.SUN) {
// TODO: work at home
}

也可以把常量定义为字符串类型,例如,定义3种颜色的常量:
1
2
3
4
5
public class Color {
public static final String RED = "r";
public static final String GREEN = "g";
public static final String BLUE = "b";
}

使用常量的时候,可以这么引用:
1
2
3
4
String color = ...
if (Color.RED.equals(color)) {
// TODO:
}

无论是int常量还是String常量,使用这些常量来表示一组枚举值的时候,有一个严重的问题就是,编译器无法检查每个值的合理性。例如:
1
2
3
4
5
if (weekday == 6 || weekday == 7) {
if (tasks == Weekday.MON) {
// TODO:
}
}

上述代码编译和运行均不会报错,但存在两个问题:

  • 注意到Weekday定义的常量范围是0~6,并不包含7,编译器无法检查不在枚举中的int值;
  • 定义的常量仍可与其他变量比较,但其用途并非是枚举星期值。

enum

为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用 enum 来定义枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}

enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}

注意到定义枚举类是通过关键字enum实现的,我们只需依次列出枚举的常量名。
和int定义的常量相比,使用enum定义枚举有如下好处:
首先,enum常量本身带有类型信息,即Weekday.SUN类型是Weekday,编译器会自动检查出类型错误。例如,下面的语句不可能编译通过:
1
2
3
int day = 1;
if (day == Weekday.SUN) { // Compile error: bad operand types for binary operator '=='
}

其次,不可能引用到非枚举的值,因为无法通过编译。
最后,不同类型的枚举不能互相比较或者赋值,因为类型不符。例如,不能给一个Weekday枚举类型的变量赋值为Color枚举类型的值:
1
2
Weekday x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types

这就使得编译器可以在编译期自动检查出所有可能的潜在错误。

enum 比较

使用 enum 定义的枚举类是一种引用类型。前面我们讲到,引用类型比较,要使用 equals() 方法,如果使用 == 比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用 equals() 方法,但 enum 类型可以例外。
这是因为 enum 类型的每个常量在 JVM 中只有一个唯一实例,所以可以直接用 == 比较:

1
2
3
4
if (day == Weekday.FRI) { // ok!
}
if (day.equals(Weekday.SUN)) { // ok, but more code!
}

enum 类型

通过enum定义的枚举类,和其他的class有什么区别?
答案是没有任何区别。enum定义的类型就是class,只不过它有以下几个特点:
定义的enum类型总是继承自java.lang.Enum,且无法被继承;

  • 只能定义出enum的实例,而无法通过new操作符创建enum的实例;
  • 定义的每个实例都是引用类型的唯一实例;
  • 可以将enum类型用于switch语句。

例如,我们定义的Color枚举类:

1
2
3
public enum Color {
RED, GREEN, BLUE;
}

编译器编译出的class大概就像这样:
1
2
3
4
5
6
7
8
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}

所以,编译后的 enum 类和普通 class 并没有任何区别。但是我们自己无法按定义普通 class 那样来定义 enum,必须使用 enum 关键字,这是 Java 语法规定的。

因为 enum 是一个 class,每个枚举的值都是 class 实例,因此,这些实例有一些方法:

name()

返回常量名,例如:

1
String s = Weekday.SUN.name(); // "SUN"

ordinal()

返回定义的常量的顺序,从0开始计数,例如:

1
int n = Weekday.MON.ordinal(); // 1

改变枚举常量定义的顺序就会导致ordinal()返回值发生变化。例如:

1
2
3
public enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}


1
2
3
public enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}

的 ordinal 就是不同的。如果在代码中编写了类似 if(x.ordinal()==1) 这样的语句,就要保证 enum 的枚举顺序不能变。新增的常量必须放在最后。
Weekday的枚举常量如果要和int转换,使用ordinal()不是非常方便?比如这样写:
1
2
String task = Weekday.MON.ordinal() + "/ppt";
saveToFile(task);

但是,如果不小心修改了枚举的顺序,编译器是无法检查出这种逻辑错误的。要编写健壮的代码,就不要依靠 ordinal() 的返回值。因为 enum 本身是 class,所以我们可以定义 private 的构造方法,并且,给每个枚举常量添加字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}

enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);

public final int dayValue;

private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}

这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个int值。

枚举类的字段也可以是非final类型,即可以在运行期修改,但是不推荐这样做!

默认情况下,对枚举常量调用toString()会返回和name()一样的字符串。但是,toString()可以被覆写,而name()则不行。我们可以给Weekday添加toString()方法:

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
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}

enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");

public final int dayValue;
private final String chinese;

private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}

@Override
public String toString() {
return this.chinese;
}
}

覆写toString()的目的是在输出时更有可读性。
判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!

switch

最后,枚举类可以应用在switch语句中。因为枚举类天生具有类型信息和有限个枚举常量,所以比int、String类型更适合用在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
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
switch(day) {
case MON:
case TUE:
case WED:
case THU:
case FRI:
System.out.println("Today is " + day + ". Work at office!");
break;
case SAT:
case SUN:
System.out.println("Today is " + day + ". Work at home!");
break;
default:
throw new RuntimeException("cannot process " + day);
}
}
}

enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}

  1. case语句中只能写枚举类定义的变量名称,不能加类名。
  2. 加上default语句,可以在漏写某个枚举常量时自动报错,从而及时发现错误。

小结

  • Java 使用 enum 定义枚举类型,它被编译器编译为 final class Xxx extends Enum {…};
  • 通过 name() 获取常量定义的字符串,注意不要使用 toString()
  • 通过 ordinal() 返回常量定义的顺序(无实质意义);
  • 可以为 enum 编写构造方法、字段和方法
  • enum 的构造方法要声明为 private,字段强烈建议声明为 final
  • enum 适合用在 switch 语句中。

BigInteger

在 Java 中,由 CPU 原生提供的整型最大范围是 64 位 long 型整数。使用 long 型整数可以直接通过 CPU 指令进行计算,速度非常快。
如果我们使用的整数范围超过了 long 型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger 就是用来表示任意大小的整数。BigInteger 内部用一个 int[] 数组来模拟一个非常大的整数:

1
2
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000

BigInteger 做运算的时候,只能使用实例方法,例如,加法运算:
1
2
3
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780

long 型整数运算比,BigInteger 不会有范围限制,但缺点是速度比较慢。

也可以把BigInteger转换成long型:

1
2
3
BigInteger i = new BigInteger("123456789000");
System.out.println(i.longValue()); // 123456789000
System.out.println(i.multiply(i).longValueExact()); // java.lang.ArithmeticException: BigInteger out of long range

使用 longValueExact() 方法时,如果超出了 long 型的范围,会抛出 ArithmeticException
BigIntegerIntegerLong 一样,也是不可变类,并且也继承自 Number 类。因为 Number 定义了转换为基本类型的几个方法:

  • 转换为 bytebyteValue()
  • 转换为 shortshortValue()
  • 转换为 intintValue()
  • 转换为 longlongValue()
  • 转换为 floatfloatValue()
  • 转换为 doubledoubleValue()

因此,通过上述方法,可以把 BigInteger 转换成基本类型。如果 BigInteger 表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用 intValueExact()longValueExact() 等方法,在转换时如果超出范围,将直接抛出 ArithmeticException 异常。

如果BigInteger的值甚至超过了float的最大范围(3.4x1038),那么返回的float是什么呢?

1
2
3
4
5
6
7
8
9
import java.math.BigInteger;

public class Main {
public static void main(String[] args) {
BigInteger n = new BigInteger("999999").pow(99);
float f = n.floatValue();
System.out.println(f);
}
}

小结

  • BigInteger 用于表示任意大小的整数;
  • BigInteger 是不变类,并且继承自 Number
  • BigInteger 转换成基本类型时可使用 longValueExact() 等方法保证结果准确。

BigDecimal

BigInteger 类似,BigDecimal 可以表示一个任意大小且精度完全准确的浮点数。

1
2
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489

BigDecimalscale() 表示小数位数,例如:
1
2
3
4
5
6
BigDecimal d1 = new BigDecimal("123.45");
BigDecimal d2 = new BigDecimal("123.4500");
BigDecimal d3 = new BigDecimal("1234500");
System.out.println(d1.scale()); // 2,两位小数
System.out.println(d2.scale()); // 4
System.out.println(d3.scale()); // 0

通过 BigDecimalstripTrailingZeros() 方法,可以将一个 BigDecimal 格式化为一个相等的,但去掉了末尾 0BigDecimal
1
2
3
4
5
6
7
8
9
BigDecimal d1 = new BigDecimal("123.4500");
BigDecimal d2 = d1.stripTrailingZeros();
System.out.println(d1.scale()); // 4
System.out.println(d2.scale()); // 2,因为去掉了00

BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d3.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2

如果一个 BigDecimalscale() 返回负数,例如,-2,表示这个数是个整数,并且末尾有 20
可以对一个 BigDecimal 设置它的 scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
1
2
3
4
5
6
7
8
9
10
11
12
import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
public static void main(String[] args) {
BigDecimal d1 = new BigDecimal("123.456789");
BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568
BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567
System.out.println(d2);
System.out.println(d3);
}
}

BigDecimal 做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
1
2
3
4
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入
BigDecimal d4 = d1.divide(d2); // 报错:ArithmeticException,因为除不尽

还可以对 BigDecimal 做除法的同时求余数:
1
2
3
4
5
6
7
8
9
10
11
import java.math.BigDecimal;

public class Main {
public static void main(String[] args) {
BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102
System.out.println(dr[1]); // 0.105
}
}

调用 divideAndRemainder() 方法时,返回的数组包含两个 BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个 BigDecimal 是否是整数倍数:
1
2
3
4
5
6
BigDecimal n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) {
// n是m的整数倍
}

比较BigDecimal

在比较两个 BigDecimal 的值是否相等时,要特别注意,使用 equals() 方法不但要求两个 BigDecimal 的值相等,还要求它们的 scale() 相等:

1
2
3
4
5
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因为scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因为d2去除尾部0后scale变为2
System.out.println(d1.compareTo(d2)); // 0

必须使用 compareTo() 方法来比较,它根据两个值的大小分别返回负数、正数和 0,分别表示小于、大于和等于。

总是使用 compareTo() 比较两个 BigDecimal 的值,不要使用 equals()

如果查看 BigDecimal 的源码,可以发现,实际上一个 BigDecimal 是通过一个 BigInteger 和一个 scale 来表示的,即 BigInteger 表示一个完整的整数,而 scale 表示小数位数:

1
2
3
4
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}

BigDecimal 也是从 Number 继承的,也是不可变对象。

小结

  • BigDecimal用于表示精确的小数,常用于财务计算;
  • 比较BigDecimal的值是否相等,必须使用compareTo()而不能使用equals()。

常用工具类

Java的核心库提供了大量的现成的类供我们使用。本节我们介绍几个常用的工具类。

Math

顾名思义,Math类就是用来进行数学计算的,它提供了大量的静态方法来便于我们实现数学计算:

求绝对值:

1
2
Math.abs(-100); // 100
Math.abs(-7.8); // 7.8

取最大或最小值:

1
2
Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2

计算 $x^y$ 次方:

1
Math.pow(2, 10); // 2的10次方=1024

计算 $\sqrt{x}$:

1
Math.sqrt(2); // 1.414...

计算 $e^x$ 次方:

1
Math.exp(2); // 7.389...

计算以 e 为底的对数:

1
Math.log(4); // 1.386...

计算以 10 为底的对数:

1
Math.log10(100); // 2

三角函数:

1
2
3
4
5
Math.sin(3.14); // 0.00159...
Math.cos(3.14); // -0.9999...
Math.tan(3.14); // -0.0015...
Math.asin(1.0); // 1.57079...
Math.acos(1.0); // 0.0

Math 还提供了几个数学常量:

1
2
3
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5

生成一个随机数 x,x 的范围是 0 <= x < 1

1
Math.random(); // 0.53907...每次都不一样

如果我们要生成一个区间在 [MIN, MAX) 的随机数,可以借助 Math.random() 实现,计算如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 区间在[MIN, MAX)的随机数
public class Main {
public static void main(String[] args) {
double x = Math.random(); // x的范围是[0,1)
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的范围是[10,50)
long n = (long) y; // n的范围是[10,50)的整数
System.out.println(y);
System.out.println(n);
}
}

Java 标准库还提供了一个 StrictMath,它提供了和 Math 几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如 x86 和 ARM)计算的结果可能不一致(指误差不同),因此,StrictMath 保证所有平台计算结果都是完全相同的,而 Math 会尽量针对平台优化计算速度,所以,绝大多数情况下,使用 Math 就足够了。

Random

Random 用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。

要生成一个随机数,可以使用 nextInt()nextLong()nextFloat()nextDouble()

1
2
3
4
5
6
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double

每次运行程序,生成的随机数都是不同的,没看出伪随机数的特性来。
这是因为我们创建Random实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
如果我们在创建Random实例时指定一个种子,就会得到完全确定的随机数序列:
1
2
3
4
5
6
7
8
9
10
11
import java.util.Random;

public class Main {
public static void main(String[] args) {
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
}

前面我们使用的Math.random()实际上内部调用了Random类,所以它也是伪随机数,只是我们无法指定种子。

SecureRandom

有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom 就是用来创建安全的随机数的:

1
2
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));

SecureRandom 无法指定种子,它使用 RNG(random number generator)算法。JDK 的 SecureRandom 实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.Arrays;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;

public class Main {
public static void main(String[] args) {
SecureRandom sr = null;
try {
sr = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器
} catch (NoSuchAlgorithmException e) {
sr = new SecureRandom(); // 获取普通的安全随机数生成器
}
byte[] buffer = new byte[16];
sr.nextBytes(buffer); // 用安全随机数填充buffer
System.out.println(Arrays.toString(buffer));
}
}

SecureRandom 的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过 CPU 的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的 “熵”。

在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用 SecureRandom 来产生安全的随机数。

需要使用安全随机数的时候,必须使用 SecureRandom,绝不能使用 Random

小结

Java提供的常用工具类有:

  • Math:数学计算
  • Random:生成伪随机数
  • SecureRandom:生成安全的随机数

面向对象基础

例子

1
2
3
4
class Person {
public String name;
public int age;
}

一个 class 可以包含多个字段(field),字段用来描述一个类的特征。上面的 Person 类,我们定义了两个字段,一个是 String 类型的字段,命名为 name,一个是 int 类型的字段,命名为 age。因此,通过 class,把一组数据汇集到一个对象上,实现了数据封装。

字段就是 C++ 中的成员属性

创建实例

1
Person ming = new Person();

实例化

1
2
3
Role role1 = new Role(); // 在堆空间里面分配了一个空间,把空间的地址赋给了 role1,用哈希码来表示在虚拟机里面的内存地址
Role role2; // 声明了一个 Role 类型的变量,叫 role2(可以把 Role 看成自己定义的数据类型),但是还没有空间
role2 = new Role(); // role2 一定要初始化,对象在运行时,一定要分配空间

类和对象的关系

  • 类是一个抽象的概念,仅仅是模板,比如说:演员、总统
  • 对象是一个你能够看得到、摸得着的具体实体
  • 类的定义者和类的使用者是不一样的。越往下越具体,越往上越抽象。

基本步骤

  1. 发现类
  2. 找出属性(名词)
  3. 找出行为(动词)
  4. 数据抽象:是数据和处理方法的结合

小结

  • 在 OOP 中,class 和 instance 是 “模版” 和 “实例” 的关系;
  • 类就是对象的模板(蓝图)
  • 定义 class 就是定义了一种数据类型,对应的 instance 是这种数据类型的实例;
  • class 定义的 field,在每个 instance 都会拥有各自的 field,且互不干扰;
  • 通过 new 操作符创建新的 instance,然后用变量指向它,即可通过变量来引用这个 instance;
  • 访问实例字段的方法是变量名. 字段名;

类图

  1. 直观、容易理解
  2. 参考工具:
    • StarUML
    • Astah
      • 号代表 public,- 号代表 private
    • 属性名在前,后面跟冒号和类型名
    • 方法名在前,后面跟冒号和返回值类型
    • 如果有参数,参数的类型写法同上
    • Astah

方法

把 field 从 public 改成 private,外部代码不能访问这些 field,以我们需要使用方法(method)来让外部代码可以间接修改 field:

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
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setName("Xiao Ming"); // 设置 name
ming.setAge(12); // 设置 age
System.out.println(ming.getName() + "," + ming.getAge());
}
}

class Person {
private String name;
private int age;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return this.age;
}

public void setAge(int age) {
if (age < 0 || age> 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
}

虽然外部代码不能直接修改 private 字段,但是,外部代码可以调用方法 setName() 和 setAge() 来间接修改 private 字段。在方法内部,我们就有机会检查参数对不对。比如,setAge() 就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把 age 设置成不合理的值。
同样,外部代码不能直接读取 private 字段,但可以通过 getName() 和 getAge() 间接获取 private 字段的值。
所以,一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。

调用方法的语法是实例变量 . 方法名 (参数);。一个方法调用就是一个语句,所以不要忘了在末尾加 ;。例如:ming.setName("Xiao Ming");

定义方法

1
2
3
4
修饰符 方法返回类型 方法名 (方法参数列表) {
若干方法语句;
return 方法返回值;
}

private 方法

有 public 方法,自然就有 private 方法。和 private 字段一样,private 方法不允许外部调用,定义 private 方法的理由是内部方法是可以调用 private 方法的。

this 变量

在方法内部,可以使用一个隐含的变量 this,它始终指向当前实例。因此,通过 this.field 就可以访问当前实例的字段。如果没有命名冲突,可以省略 this,但是如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上 this

方法参数

方法可以包含 0 个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。(同 C++)

可变参数

可变参数用 类型... 定义,可变参数相当于数组类型:

1
2
class Group {
private String[] names;

  public void setNames(String... names) {
     this.names = names;
  }

}

1
2
3
4
5
6
7
> 上面的 setNames() 就定义了一个可变参数。调用时,可以这么写:
```java
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入 3 个 String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入 2 个 String
g.setNames("Xiao Ming"); // 传入 1 个 String
g.setNames(); // 传入 0 个 String

完全可以把可变参数改写为 String[] 类型:

1
2
class Group {
private String[] names;

  public void setNames(String[] names) {
     this.names = names;
  }

}

1
2
3
4
> 但是,调用方需要自己先构造 String[],比较麻烦。例如:
```java
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入 1 个 String[]

另一个问题是,调用方可以传入 null:

1
2
Group g = new Group();
g.setNames(null);

而可变参数可以保证无法传入 null,因为传入 0 个参数时,接收到的实际值是一个空数组而不是 null。

参数绑定

传参的问题
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型的传递:将地址进行复制,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入 fullname 数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname 数组的第一个元素修改为 "Bart"
System.out.println(p.getName()); // "Bart Simpson"
}
}

class Person {
private String[] name;

public String getName() {
return this.name[0] + " " + this.name[1];
}

public void setName(String[] name) {
this.name = name;
}
}

Note: 因为传入的是字符串数组,因此将 fullname 数组的地址复制,传给了 p.name,因此 p.namefullname 指向的是同一个字符串数组,所以这两个同时变化,但是变化的时候还是遵循引用的原则:Homer 仍然存在,只是无法通过 fullname[0] 进行访问罢了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入 bob 变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob 改名为 Alice
System.out.println(p.getName()); // "Bob"
}
}

class Person {
private String name;

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}
}

Note: 和上一个传入引用参数一样,复制的是地址,传入之后 bobp.name 指向了同一块内存,只不过 bob 改变之后会重新指向新的内存,所以这两个变量指向的内存就不一样了。

Note:字符串和数组都是引用类型。

构造方法

  1. 由于构造方法是如此特殊,所以构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有 void),调用构造方法,必须用 new 操作符。(具体意义和 C++ 一样)
  2. 没有在构造方法中初始化字段时,引用类型的字段默认是 null,数值类型的字段用默认值,int 类型默认值是 0,布尔类型默认值是 false:
  3. 可以对字段直接进行初始化:
1
2
3
4
5
6
7
8
9
class Person {
private String name = "Unamed";
private int age = 10;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

构造方法的代码由于后运行,最终由构造方法的代码确定,即便已经直接将字段初始化了。

方法重载

> 同 C++

继承

Student 类包含了 Person 类已有的字段和方法,只是多出了一个 score 字段和相应的 getScore()、setScore() 方法。
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让 Student 从 Person 继承时,Student 就获得了 Person 的所有功能,我们只需要为 Student 编写新增的功能。

实现

Java 使用 extends 关键字来实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
private String name;
private int age;

public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}

class Student extends Person {
// 不要重复 name 和 age 字段 / 方法,
// 只需要定义新增 score 字段 / 方法:
private int score;

public int getScore() { …}
public void setScore(int score) { … }
}

通过继承,Student 只需要编写额外的功能,不再需要重复代码。

Note: 子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

在 OOP 的术语中,我们把 Person 称为 超类(super class) 父类(parent class) 基类(base class),把 Student 称为 子类(subclass) 扩展类(extended class)

继承树

在 Java 中,没有明确写 extends 的类,编译器会自动加上 extends Object。所以,任何类,除了 Object,都会继承自某个类。
Java 只允许一个 class 继承自一个类,因此,一个类有且仅有一个父类。只有 Object 特殊,它没有父类。

例如:

protected

继承有个特点,就是子类无法访问父类的 private 字段或者 private 方法。
为了让子类可以访问父类的字段,我们需要把 private 改为 protected。用 protected 修饰的字段可以被子类访问: 因此,protected 关键字可以把字段和方法的访问权限控制在继承树内部,一个 protected 字段和方法可以被其子类,以及子类的子类所访问。

super

super 关键字表示父类(超类)。子类引用父类的字段时,可以用 super.fieldName。例如:

1
2
3
4
5
class Student extends Person {
public String hello() {
return "Hello," + super.name;
}
}

实际上,这里使用 super.name,或者 this.name,或者 name,效果都是一样的。编译器会自动定位到父类的 name 字段。
但是,在某些时候,就必须使用 super。我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 12, 89);
}
}

class Person {
protected String name;
protected int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

class Student extends Person {
protected int score;

public Student(String name, int age, int score) {
this.score = score;
}
}

运行上面的代码,会得到一个 编译错误,大意是在 Student 的构造方法中,无法调用 Person 的构造方法。

这是因为在 Java 中,任何 class 的构造方法,**第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句 super();**,所以,Student 类的构造方法实际上是这样:

1
2
3
4
5
6
7
8
class Student extends Person {
protected int score;

public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
this.score = score;
}
}

但是,Person 类并没有无参数的构造方法,因此,编译失败。

解决方法是调用 Person 类存在的某个构造方法。例如:

1
2
3
4
5
6
7
8
class Student extends Person {
protected int score;

public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法 Person(String, int)
this.score = score;
}
}

如果父类没有默认的构造方法,子类就必须显式调用 super() 并给出参数以便让编译器定位到父类的一个合适的构造方法。

这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

只要某个 class 没有 final 修饰符,那么任何类都可以从该 class 继承。
从 Java 15 开始,允许使用 sealed 修饰 class,并通过 permits 明确写出能够从该 class 继承的子类名称。

1
2
3
public sealed class Shape permits Rect, Circle, Triangle {
...
}

上述 Shape 类就是一个 sealed 类,它只允许指定的 3 个类 (Rect, Circle, Triangle) 继承它,否则就会报错。
这种 sealed 类主要用于一些框架,防止继承被滥用。sealed 类在 Java 15 中目前是预览状态,要启用它,必须使用参数 --enable-preview--source 15

向上转型:一个子类类型安全地变为父类类型的赋值

向上转型实际上是把一个子类型安全地变为更加抽象的父类型

1
2
3
4
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok

p 也只能使用 Person 类中有的字段和方法,不能使用 Student 添加的字段和方法

向下转型:把一个父类类型强制转型为子类类型

不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

instanceof 实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为 null,那么对任何 instanceof 的判断都为 false

1
2
3
4
5
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}

从 Java 14 开始,判断 instanceof 后,可以直接转型为指定变量,避免再次强制转型。

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
Object obj = "hello";
if (obj instanceof String s) {
// 可以直接使用变量 s:
System.out.println(s.toUpperCase());
}
}
}

等价于

1
2
3
4
5
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}

组合和继承

1
2
3
4
5
class Book {
protected String name;
public String getName() {...}
public void setName(String name) {...}
}

这个 Book 类也有 name 字段,那么,我们能不能让 Student 继承自 Book 呢?不可以
从逻辑上讲,这是不合理的,Student 不应该从 Book 继承,而应该从 Person 继承。
究其原因,是因为 Student 是 Person 的一种,它们是 is 关系,而 Student 并不是 Book。实际上 Student 和 Book 的关系是 has 关系。
具有 has 关系不应该使用继承,而是使用组合,即 Student 可以持有一个 Book 实例:

1
2
3
4
class Student extends Person {
protected Book book;
protected int score;
}

继承是 is 关系,组合是 has 关系。

多态

补充

方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成。

Note:方法签名不包括方法的返回类型。不包括返回值和访问修饰符。

覆写(重写、覆盖)

  1. 在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。

  2. 重写(Override)和重载(Overload)
    Override 和 Overload 不同的是,如果方法签名如果不同,就是 Overload,Overload 方法是一个新方法;如果方法签名相同,并且返回值也相同,就是 Override

    重写: 前提是继承,两个方法的方法签名相同。至于修饰符,范围相同或者比父类的范围大即可。

    要是重写的话,也不是必须加 @override,写 @override,这个表示编辑器会给你去父类检查重写的对不对,不写就自己检查呗。

    重写是覆盖,就是子类的替换了父类的,正常用方法,调的是子类的,所以重写完还是一个方法
    重载是写多个同名方法,但可以给的参数不同,功能类似,所以正常调哪个方法,要看参数怎么写的,跟继承无关,区别是重载后变成多个方法。

  3. 注意

    1. 父类:如果是 private,那么子类就不存在重写,只是新建了个方法。但是要是 protected 的话,子类就可以重写。@override 是可以 OK 的。子类是 protected 或者 public,不可以是 private(经测试 private 类型是错误的,直接报错)。
    2. 在修饰符条件 OK 的情况下,发现如果可继承的要重写的方法要是返回类型不同,编辑器也是会报错的。说是重写的两个方法的返回类型冲突、不相容的,矛盾的返回类型。

引例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run(); // 打印 Student.run
}
}

class Person {
public void run() {
System.out.println("Person.run");
}
}

class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}

Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。

多态定义

针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。

1
2
3
public void runTwice(Person p) {
p.run();
}

它传入的参数类型是 Person,我们是无法知道传入的参数实际类型究竟是 Person,还是 Student,还是 Person 的其他子类,因此,也无法确定调用的是不是 Person 类定义的 run() 方法。
所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。

多态的用处

我们要编写一个报税的财务软件,对于一个人的所有收入进行报税

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
// 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
Income[] incomes = new Income[] {
new Income(3000),
new Salary(7500),
new StateCouncilSpecialAllowance(15000)
};
System.out.println(totalTax(incomes));
}

  public static double totalTax(Income... incomes) {
     double total = 0;
     for (Income income: incomes) {
           total = total + income.getTax();
     }
     return total;
  }

}

class Income {
protected double income;

  public Income(double income) {
     this.income = income;
  }

  public double getTax() {
     return income * 0.1; // 税率 10%
  }

}

class Salary extends Income {
public Salary(double income) {
super(income);
}

  @Override
  public double getTax() {
     if (income <= 5000) {
           return 0;
     }
     return (income - 5000) * 0.2;
  }

}

class StateCouncilSpecialAllowance extends Income {
public StateCouncilSpecialAllowance(double income) {
super(income);
}

  @Override
  public double getTax() {
     return 0;
  }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

> 观察 totalTax() 方法:利用多态,totalTax() 方法只需要和 Income 打交道,它完全不需要知道 Salary 和 StateCouncilSpecialAllowance 的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从 Income 派生,然后正确覆写 getTax() 方法就可以。把新的类型传入 totalTax(),不需要修改任何代码。

> *** 可见,多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。***


## 调用 super
```java
class Person {
protected String name;
public String hello() {
return "Hello," + name;
}
}

Student extends Person {
@Override
public String hello() {
// 调用父类的 hello() 方法:
return super.hello() + "!";
}
}

final

  1. 继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为 final。用 final 修饰的方法不能被 Override:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person {
    protected String name;
    public final String hello() {
    return "Hello," + name;
    }
    }

    Student extends Person {
    // compile error: 不允许覆写
    @Override
    public String hello() {
    }
    }
  2. 如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为 final。用 final 修饰的类不能被继承:
    1
    2
    3
    4
    5
    6
    7
    final class Person {
    protected String name;
    }

    // compile error: 不允许继承自 Person
    Student extends Person {
    }
  3. 对于一个类的实例字段,同样可以用 final 修饰。用 final 修饰的字段在初始化后不能被修改。
    1
    2
    3
    class Person {
    public final String name = "Unamed";
    }
    对 final 字段重新赋值会报错:
    1
    2
    Person p = new Person();
    p.name = "New Name"; // compile error!
    这样也不可以:
    1
    2
    3
    4
    5
    6
    7
    public class Person {
    public final String name = "Unamed";

    public TestDemo1(String name) {
    this.name = name;
    }
    }
    最常用的方法:
    1
    2
    3
    4
    5
    6
    class Person {
    public final String name;
    public Person(String name) {
    this.name = name;
    }
    }

    因为可以保证实例一旦创建,其 final 字段就不可修改:

抽象类

引入

因为可以保证实例一旦创建,其 final 字段就不可修改:

1
2
3
class Person {
public void run() { …}
}

class Student extends Person {
@Override
public void run() { …}
}

class Teacher extends Person {
@Override
public void run() { …}
}

1
2
3
4
5
6
> 从 Person 类派生的 Student 和 Teacher 都可以覆写 run() 方法。
> 如果父类 Person 的 run() 方法没有实际意义,能否去掉方法的执行语句?
```java
class Person {
public void run(); // Compile Error!
}

答案是不行,会导致编译错误,因为定义方法的时候,必须实现方法的语句。
而且不可以去掉父类的 run() 方法,因为如果去掉了父类的 run() 方法,就会失去多态的特性

解决办法:如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

1
2
3
class Person {
public abstract void run();
}

把一个方法声明为 abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person 类也无法被实例化。编译器会告诉我们,无法编译 Person 类,因为它包含抽象方法,必须把 Person 类本身也声明为 abstract,才能正确编译它:
1
2
3
abstract class Person {
public abstract void run();
}

个人理解

1
2
3
abstract class Person {
public abstract void run();
}

等价于

1
2
3
class Person {
public void run() {}
}

区别在于:
前者不可以实例化,后者可以实例化

定义

如果一个 class 定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用 abstract 修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。

使用 abstract 修饰的类就是抽象类。我们无法实例化一个抽象类:

1
Person p = new Person(); // 编译错误

抽象类的用途

因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了 “规范”。

例如,Person 类定义了抽象方法 run(),那么,在实现子类 Student 的时候,就必须覆写 run() 方法:

面向抽象编程

当我们定义了抽象类 Person,以及具体的 Student、Teacher 子类的时候,我们可以通过抽象类 Person 类型去引用具体的子类的实例:

1
2
Person s = new Student();
Person t = new Teacher();

这种引用抽象类的好处在于,我们对其进行方法调用,并不关心 Person 类型变量的具体子类型:
1
2
3
// 不关心 Person 变量的具体子类型:
s.run();
t.run();

同样的代码,如果引用的是一个新的子类,我们仍然不关心具体类型:
1
2
3
// 同样不关心新的子类是如何实现 run() 方法的:
Person e = new Employee();
e.run();

这种尽量引用 ** 高层类型 **(相当于父类),避免引用 ** 实际子类型 **(相当于子类)的方式,称之为面向抽象编程。

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person);
  • 不需要子类就可以实现业务逻辑(正常编译);
  • 具体的业务逻辑由不同的子类实现,调用者并不关心(个人理解:就是调用者只需要知道抽象类有什么字段和方法以及功能,不关心具体怎么实现的,可以直接调用,具体实现对调用者透明)。

小结

  • 通过 abstract 定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
  • 定义了抽象方法的 class 必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
  • 如果不实现抽象方法,则该子类仍是一个抽象类;
  • 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。

接口

引入

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法:

1
2
3
4
abstract class Person {
public abstract void run();
public abstract String getName();
}

就可以把该抽象类改写为接口: interface
1
2
3
4
interface Person {
void run();
String getName();
}

定义

所谓 interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是 public abstract 的,所以这两个修饰符不需要写出来(写不写效果都一样)。

当一个具体的 class 去实现一个 interface 时,需要使用 implements 关键字。举个例子:

1
2
class Student implements Person {
private String name;

  public Student(String name) {
     this.name = name;
  }

  @Override
  public void run() {
     System.out.println(this.name + "run");
  }

  @Override
  public String getName() {
     return this.name;
  }

}

1
2
3
4
5
> 在 Java 中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个 interface,例如:
```java
class Student implements Person, Hello { // 实现了两个 interface
...
}

术语

Java 的接口特指 interface 的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。

抽象类和接口的对比如下:

abstract class interface
继承 只能 extends 一个 class 可以 implements 多个 interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义 default 方法

接口继承

一个 interface 可以继承自另一个 interface。interface 继承自 interface 使用 extends,它相当于扩展了接口的方法。例如:

1
2
3
4
5
6
7
8
9
10
interface Hello {
void hello();
}


interface Person extends Hello {
void run();
String getName();
}

此时,Person 接口继承自 Hello 接口,因此,Person 接口现在实际上有 3 个抽象方法签名,其中一个来自继承的 Hello 接口。

继承关系

合理设计 interface 和 abstract class 的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在 abstract class 中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考 Java 的集合类定义的一组接口、抽象类以及具体子类的继承关系:

在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
1
2
3
List list = new ArrayList(); // 用 List 接口引用具体子类的实例
Collection coll = list; // 向上转型为 Collection 接口
Iterable it = coll; // 向上转型为 Iterable 接口

default 方法

在接口中,可以定义 default 方法。例如,把 Person 接口的 run() 方法改为 default 方法:

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Person p = new Student("Xiao Ming");
p.run();
}
}

interface Person {
String getName();
default void run() {
System.out.println(getName() + “run”);
}
}

class Student implements Person {
private String name;

  public Student(String name) {
     this.name = name;
  }

  public String getName() {
     return this.name;
  }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   > 实现类可以不必覆写 default 方法。default 方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是 default 方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
> default 方法和抽象类的普通方法是有所不同的。因为 interface 没有字段,default 方法无法访问字段,而抽象类的普通方法可以访问实例字段。

## 小结
- Java 的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;
- 接口也是数据类型,适用于向上转型和向下转型;
- 接口的所有方法都是抽象方法,接口不能定义实例字段;
- 接口可以定义 default 方法(JDK>=1.8)。


# 静态字段和静态方法

## 静态字段
> 在一个 class 中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。

> 还有一种字段,是用 static 修饰的字段,称为静态字段:static field。
> 实例字段在每个实例中都有自己的一个独立 “空间”,但是静态字段只有一个共享 “空间”,所有实例都会共享该字段。举个例子:
```java
class Person {
public String name;
public int age;
// 定义静态字段 number:
public static int number;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) {
Person ming = new Person("Xiao Ming", 12);
Person hong = new Person("Xiao Hong", 15);
ming.number = 88;
System.out.println(hong.number);
hong.number = 99;
System.out.println(ming.number);
}
}

class Person {
public String name;
public int age;

public static int number;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

输出

1
2
88
99

对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例:

虽然实例可以访问静态字段,但是它们指向的其实都是 Person class 的静态字段。所以,所有实例共享一个静态字段。
因此,不推荐用 实例变量. 静态字段 去访问静态字段,因为在 Java 程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名. 静态字段来访问静态对象。

推荐用类名来访问静态字段。可以把静态字段理解为描述 class 本身的字段(非实例字段)。对于上面的代码,更好的写法是:

1
2
Person.number = 99;
System.out.println(Person.number);

静态方法

有静态字段,就有静态方法。用 static 修饰的方法称为静态方法。(废话)
调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。例如:

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Person.setNumber(99);
System.out.println(Person.number);
}
}

class Person {
public static int number;

  public static void setNumber(int value) {
     number = value;
  }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   > 因为静态方法属于 class 而不属于实例,因此,静态方法内部,无法访问 this 变量,也无法访问实例字段,它只能访问静态字段。
> 通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。
> 通常情况下,通过实例变量访问静态字段和静态方法,会得到一个编译警告。

> 静态方法经常用于工具类,例如:
- `Arrays.sort()`
- `Math.random()`
> 静态方法也经常用于辅助方法。注意到 Java 程序的入口 main() 也是静态方法。

## 接口的静态字段
> 因为 interface 是一个纯抽象类,所以它不能定义实例字段。但是,interface 是可以有静态字段的,并且静态字段必须为 final 类型:
```java
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}

实际上,因为 interface 的字段只能是 public static final 类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:

1
2
3
4
5
public interface Person {
// 编译器会自动加上 public statc final:
int MALE = 1;
int FEMALE = 2;
}

编译器会自动把该字段变为 public static final 类型。

小结

  • 静态字段属于所有实例 “共享” 的字段,实际上是属于 class 的字段;
  • 调用静态方法不需要实例,无法访问 this,但可以访问静态字段和其他静态方法;
  • 静态方法常用于工具类和辅助方法。

定义

在前面的代码中,我们把类和接口命名为 PersonStudentHello 等简单名字。

在现实中,如果小明写了一个 Person 类,小红也写了一个 Person 类,现在,小白既想用小明的 Person,也想用小红的 Person,怎么办?

如果小军写了一个 Arrays 类,恰好 JDK 也自带了一个 Arrays 类,如何解决类名冲突?

在 Java 中,我们使用 package 来解决名字冲突。

Java 定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如 Person)只是一个简写,真正的完整类名是包名. 类名。

例如:

小明的 Person 类存放在包 ming 下面,因此,完整类名是 ming.Person

小红的 Person 类存放在包 hong 下面,因此,完整类名是 hong.Person

小军的 Arrays 类存放在包 mr.jun 下面,因此,完整类名是 mr.jun.Arrays

JDK 的 Arrays 类存放在包 java.util 下面,因此,完整类名是 java.util.Arrays

在定义 class 的时候,我们需要在第一行声明这个 class 属于哪个包。

小明的 Person.java 文件

1
package ming; // 申明包名 ming

public class Person {

}

1
2
3
4
5
6
7
8

> 小军的 `Arrays.java` 文件
```java
package mr.jun; // 申明包名 mr.jun

public class Arrays {

}

在 Java 虚拟机执行的时候,JVM 只看完整类名,因此,只要包名不同,类就不同。

包可以是多层结构,用 . 隔开。例如:java.util

要特别注意:包没有父子关系。java.utiljava.util.zip 是不同的包,两者没有任何继承关系。

没有定义包名的 class,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。

我们还需要按照包结构把上面的 Java 文件组织起来。假设以 package_sample 作为根目录,src 作为源码目录,那么所有文件结构就是:

1
2
3
4
5
6
7
8
9
package_sample
└─ src
├─ hong
│ └─ Person.java
│ ming
│ └─ Person.java
└─ mr
└─ jun
└─ Arrays.java

即所有 Java 文件对应的目录层次要和包的层次一致。

编译后的 .class 文件也需要按照包结构存放。如果使用 IDE,把编译后的 .class 文件放到 bin 目录下,那么,编译的文件结构就是:

1
2
3
4
5
6
7
8
9
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class

编译的命令相对比较复杂,我们需要在 src 目录下执行 javac 命令:

1
javac -d ../bin ming/Person.java hong/Person.java mr/jun/Arrays.java

在 IDE 中,会自动根据包结构编译所有 Java 源码,所以不必担心使用命令行编译的复杂命令。

包作用域

位于同一个包的类,可以访问包作用域的字段和方法。不用 public、protected、private 修饰的字段和方法就是包作用域。例如,Person 类定义在 hello 包下面:

1
package hello;

public class Person {
// 包作用域:
void hello() {
System.out.println(“Hello!”);
}
}

1
2
3
4
5
6
7
8
9
10
> Main 类也定义在 hello 包下面:
```java
package hello;

public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello(); // 可以调用,因为 Main 和 Person 在同一个包
}
}

import

在一个 class 中,我们总会引用其他的 class。例如,小明的 ming.Person 类,如果要引用小军的 mr.jun.Arrays 类,他有三种写法:

第一种,直接写出完整类名,例如:

1
2
// Person.java
package ming;

public class Person {
public void run() {
mr.jun.Arrays arrays = new mr.jun.Arrays();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> 很显然,每次写完整类名比较痛苦。
> 因此,第二种写法是用 import 语句,导入小军的 Arrays,然后写简单类名:
```java
// Person.java
package ming;

// 导入完整类名:
import mr.jun.Arrays;

public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}

在写 import 的时候,可以使用 *,表示把这个包下面的所有 class 都导入进来(但不包括子包的 class):

1
2
// Person.java
package ming;

// 导入 mr.jun 包的所有 class:
import mr.jun.*;

public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> 我们一般不推荐这种写法,因为在导入了多个包后,很难看出 Arrays 类属于哪个包。

> 还有一种 import static 的语法,它可以导入可以导入一个类的静态字段和静态方法:
```java
package main;

// 导入 System 类的所有静态字段和静态方法:
import static java.lang.System.*;

public class Main {
public static void main(String[] args) {
// 相当于调用 System.out.println(…)
out.println("Hello, world!");
}
}

import static 很少使用。

Java 编译器最终编译出的 .class 文件只使用完整类名,因此,在代码中,当编译器遇到一个 class 名称时:

  • 如果是完整类名,就直接根据完整类名查找这个 class;
  • 如果是简单类名,按下面的顺序依次查找:
    • 查找当前 package 是否存在这个 class;
    • 查找 import 的包是否包含这个 class;
    • 查找 java.lang 包是否包含这个 class。

如果按照上面的规则还无法确定类名,则编译报错。

编写 class 的时候,编译器会自动帮我们做两个 import 动作:

  • 默认自动 import 当前 package 的其他 class;
  • 默认自动 import java.lang.*

** 注意 **:

  • 自动导入的是 java.lang 包,但类似 java.lang.reflect 这些包仍需要手动导入。
  • 如果有两个 class 名称相同,例如,mr.jun.Arrays 和 java.util.Arrays,那么只能 import 其中一个,另一个必须写完整类名。

最佳实践

为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:

  • org.apache
  • org.apache.commons.log
  • com.liaoxuefeng.sample

    子包就可以根据功能自行命名。

要注意不要和 java.lang 包的类重名,即自己的类不要使用这些名字:

  • String
  • System
  • Runtime
  • ...

要注意也不要和 JDK 常用类重名:

  • java.util.List
  • java.text.Format
  • java.math.BigInteger
  • ...

小结

  • Java 内建的 package 机制是为了避免 class 命名冲突;
  • JDK 的核心类使用 java.lang 包,编译器会自动导入;
  • JDK 的其它常用类定义在 java.util.*,java.math.*,java.text.*,……;
  • 包名推荐使用倒置的域名,例如 org.apache。

作用域

public、protected、private 这些修饰符。在 Java 中,这些修饰符可以用来限定访问作用域。

public

定义为 public 的 class、interface 可以被其他任何类访问:

1
package abc;

public class Hello {
public void hi() {
}
}

1
2
3
4
5
6
7
8
9
10
11
> 上面的 Hello 是 public,因此,可以被其他包的类访问:

```java
package xyz;

class Main {
void foo() {
// Main 可以访问 Hello
Hello h = new Hello();
}
}


定义为 public 的 field、method 可以被其他类访问,前提是首先有访问 class 的权限:

1
package abc;

public class Hello {
public void hi() {
}
}

1
2
3
4
5
6
7
8
9
10
> 上面的 hi() 方法是 public,可以被其他类调用,前提是首先要能访问 Hello 类:
```java
package xyz;

class Main {
void foo() {
Hello h = new Hello();
h.hi();
}
}

private

定义为 private 的 field、method 无法被其他类访问:

1
package abc;

public class Hello {
// 不能被其他类调用:
private void hi() {
}

  public void hello() {
     this.hi();
  }

}

1
2
3
4
5
6
7
8
9
10
11
12
> ** 实际上,确切地说,private 访问权限被限定在 class 的内部,而且与方法声明顺序无关。推荐把 private 方法放到后面,因为 public 方法定义了类对外提供的功能,阅读代码的时候,应该先关注 public 方法:**
```java
package abc;

public class Hello {
public void hello() {
this.hi();
}

private void hi() {
}
}

由于 Java 支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问 private 的权限:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}

  // private 方法:
  private static void hello() {
     System.out.println("private hello!");
  }

  // 静态内部类:
  static class Inner {
     public void hi() {
           Main.hello();
     }
  }

}

1
2
3
4
5
6
7
8
9
10
11
12
   > 定义在一个 `class` 内部的 `class` 称为嵌套类(nested class),Java 支持好几种嵌套类。经过自己测试: `static` 不能没有

## protected
> `protected` 作用于继承关系。定义为 `protected` 的字段和方法可以被子类访问,以及子类的子类:
```java
package abc;

public class Hello {
// protected 方法:
protected void hi() {
}
}

上面的 protected 方法可以被继承的类访问:

1
package xyz;

class Main extends Hello {
void foo() {
// 可以访问 protected 方法:
hi();
}
}

1
2
3
4
5
6
7
8
9
10
11

## package
> 包作用域是指一个类允许访问同一个 `package` 的没有 `public`、`private` 修饰的 `class`,以及没有 `public`、`protected`、`private` 修饰的字段和方法。但是不允许包之外的类访问。
```java
package abc;
// package 权限的类:
class Hello {
// package 权限的方法:
void hi() {
}
}

只要在同一个包,就可以访问 package 权限的 class、field 和 method:

1
package abc;

class Main {
void foo() {
// 可以访问 package 权限的类:
Hello h = new Hello();
// 可以调用 package 权限的方法:
h.hi();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   > 注意,包名必须完全一致,包没有父子关系,`com.apache` 和 `com.apache.abc` 是不同的包

## 局部变量
> 在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
```java
package abc;

public class Hello {
void hi(String name) { // ①
String s = name.toLowerCase(); // ②
int len = s.length(); // ③
if (len < 10) { // ④
int p = 10 - len; // ⑤
for (int i=0; i<10; i++) { // ⑥
System.out.println(); // ⑦
} // ⑧
} // ⑨
} // ⑩
}

我们观察上面的 hi() 方法代码:

  • 方法参数 name 是局部变量,它的作用域是整个方法,即 ①~⑩
  • 变量 s 的作用域是定义处到方法结束,即 ②~⑩
  • 变量 len 的作用域是定义处到方法结束,即 ③~⑩
  • 变量 p 的作用域是定义处到 if 块结束,即 ⑤~⑨
  • 变量 i 的作用域是 for 循环,即 ⑥~⑧

final

final 与访问权限不冲突,它有很多作用。

  1. final 修饰 class 可以阻止被继承:
    1
    package abc;

// 无法被继承:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}

1
2
3
4
5
6
7
8
9
10

> 2. 用 `final` 修饰 `method` 可以阻止被子类覆写:
```java
package abc;

public class Hello {
// 无法被覆写:
protected final void hi() {
}
}

  1. final 修饰 field 可以阻止被重新赋值:
    1
    package abc;

public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!
}
}

1
2
3
4
5
6
7
8
9
10

> 4. 用 final 修饰局部变量可以阻止被重新赋值:
```java
package abc;

public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}

最佳实践

  • 如果不确定是否需要 public,就不声明为 public,即尽可能少地暴露对外的字段和方法。
  • 把方法定义为 package 权限有助于测试,因为测试类和被测试类只要位于同一个 package,测试代码就可以访问被测试类的 package 权限方法。
  • 一个. java 文件只能包含一个 public 类,但可以包含多个非 public 类。如果有 public 类,文件名必须和 public 类的名字相同。

小结

  • Java 内建的访问权限包括 publicprotectedprivatepackage 权限;

  • Java 在方法内部定义的变量是局部变量,局部变量的作用域从变量声明开始,到一个块结束;

  • final 修饰符不是访问权限,它可以修饰 classfieldmethod

  • 一个 .java 文件只能包含一个 public 类,但可以包含多个非 public 类。

  • 关于修饰符的问题:

    • public:包内、包外(需要 import)都可以访问
    • private:只有类的内部才可以访问
    • protected:只有类、子类、子类的子类可以访问
    • 没有修饰符:只有包的内部能访问,包外不可以访问

内部类

通常情况下,我们把不同的类组织在不同的包下面,对于一个包下面的类来说,它们是在同一层次,没有父子关系:

1
2
3
4
5
java.lang
├── Math
├── Runnable
├── String
└── ...

还有一种类,它被定义在另一个类的内部,所以称为内部类(Nested Class)。Java 的内部类分为好几种,通常情况用得不多,但也需要了解它们是如何使用的。

Inner Class

如果一个类定义在另一个类的内部,这个类就是 Inner Class

1
2
3
4
5
class Outer {
class Inner {
// 定义了一个 Inner Class
}
}

上述定义的 Outer 是一个普通类,而 Inner 是一个 Inner Class,它与普通类有个最大的不同,就是 Inner Class 的实例不能单独存在,必须依附于一个 Outer Class 的实例。示例代码如下:
1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // 实例化一个 Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个 Inner
inner.hello();
System.out.println(outer.age);
}
}

class Outer {
private String name;
public int age;

  Outer(String name) {
     this.name = name;
  }

  class Inner {
     void hello() {
           System.out.println("Hello," + Outer.this.name);
           Outer.this.age =100; // 或者 age = 10;
     }
  }

}

1
2
3
> 观察上述代码,要实例化一个 Inner,我们必须首先创建一个 Outer 的实例,然后,调用 Outer 实例的 new 来创建 Inner 实例:
```java
Outer.Inner inner = outer.new Inner();

这是因为 Inner Class 除了有一个 this 指向它自己,还隐含地持有一个 Outer Class 实例,可以用 Outer.this 访问这个实例。所以,实例化一个 Inner Class 不能脱离 Outer 实例。
Inner Class 和普通 Class 相比,除了能引用 Outer 实例外,还有一个额外的 “特权”,就是可以修改 Outer Class 的 private 字段,因为 Inner Class 的作用域在 Outer Class 内部,所以能访问 Outer Class 的 private 字段和方法。

观察 Java 编译器编译后的. class 文件可以发现,Outer 类被编译为 Outer.class,而 Inner 类被编译为 Outer$Inner.class。

Anonymous Class (?)

还有一种定义 Inner Class 的方法,它不需要在 Outer Class 中明确地定义这个 Class,而是在方法内部,通过匿名类(Anonymous Class)来定义。示例代码如下:

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
outer.asyncHello();
}
}

class Outer {
private String name;

  Outer(String name) {
     this.name = name;
  }

  void asyncHello() {
     Runnable r = new Runnable() {
           @Override
           public void run() {
              System.out.println("Hello," + Outer.this.name);
           }
     };
     new Thread(r).start();
  }

}

1
2
3
4
5
> 观察 `asyncHello()` 方法,我们在方法内部实例化了一个 `Runnable`。`Runnable` 本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了 `Runnable` 接口的匿名类,并且通过 new 实例化该匿名类,然后转型为 `Runnable`。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:
```java
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};

匿名类和 Inner Class 一样,可以访问 Outer Class 的 private 字段和方法。之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义 Inner Class 可以少写很多代码。
观察 Java 编译器编译后的. class 文件可以发现,Outer 类被编译为 Outer.class,而匿名类被编译为 Outer1.class。如果有多个匿名类,Java 编译器会将每个匿名类依次命名为 Outer1、Outer2、Outer3……

除了接口外,匿名类也完全可以继承自普通类。观察以下代码:

1
import java.util.HashMap;

public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 匿名类!
HashMap<String, String> map3 = new HashMap<>() {
{
put(“A”, “1”);
put(“B”, “2”);
}
};
System.out.println(map3.get(“A”));
}
}

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
   > map1 是一个普通的 HashMap 实例,但 map2 是一个匿名类实例,只是该匿名类继承自 HashMap。map3 也是一个继承自 HashMap 的匿名类实例,并且添加了 static 代码块来初始化数据。观察编译输出可发现 Main$1.class 和 Main$2.class 两个匿名类文件。

## Static Nested Class
> 最后一种内部类和 Inner Class 类似,但是使用 static 修饰,称为静态内部类(Static Nested Class):
```java
public class Main {
public static void main(String[] args) {
Outer.StaticNested sn = new Outer.StaticNested();
sn.hello();
}
}

class Outer {
private static String NAME = "OUTER";

private String name;

Outer(String name) {
this.name = name;
}

static class StaticNested {
void hello() {
System.out.println("Hello," + Outer.NAME);
}
}
}

用 static 修饰的内部类和 Inner Class 有很大的不同,它不再依附于 Outer 的实例,而是一个完全独立的类,因此无法引用 Outer.this,但它可以访问 Outer 的 private 静态字段和静态方法。如果把 StaticNested 移到 Outer 之外,就失去了访问 private 的权限。

但是在实例化 静态内部类 的时候,也会实例化一个 Outer

小结

Java 的内部类可分为 Inner Class、Anonymous Class 和 Static Nested Class 三种:

  • Inner Class 和 Anonymous Class 本质上是相同的,都必须依附于 Outer Class 的实例,即隐含地持有 Outer.this 实例,并拥有 Outer Class 的 private 访问权限;
  • Static Nested Class 是独立类,但拥有 Outer Class 的 private 访问权限。

补充

内部类就是在一个类的内部再定义一个类。比如:A类中定义一个B类,那么B类相对于A类来说就称为内部类,而A类相对于B类来说就是外部类了。

classpath 和 jar

classpath

在 Java 中,我们经常听到 classpath 这个东西。网上有很多关于 “如何设置 classpath” 的文章,但大部分设置都不靠谱。到底什么是 classpath
classpathJVM 用到的一个环境变量,它用来指示 JVM 如何搜索 class

因为 Java 是编译型语言,源码文件是 .java,而编译后的 .class 文件才是真正可以被 JVM 执行的字节码。因此,JVM 需要知道,如果要加载一个 abc.xyz.Hello 的类,应该去哪搜索对应的 Hello.class 文件。
所以,classpath 就是一组目录的集合,它设置的搜索路径与操作系统相关。例如,在 Windows 系统上,用 ; 分隔,带空格的目录用 "" 括起来,可能长这样:

1
C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"

在 Linux 系统上,用: 分隔,可能长这样:
1
/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin

现在我们假设 classpath.;C:\work\project1\bin;C:\shared,当 JVM 在加载 abc.xyz.Hello 这个类时,会依次查找:

  • <当前目录>\abc\xyz\Hello.class
  • C:\work\project1\bin\abc\xyz\Hello.class
  • C:\shared\abc\xyz\Hello.class

    注意到 . 代表当前目录。如果 JVM 在某个路径下找到了对应的 class 文件,就不再往后继续搜索。如果所有路径下都没有找到,就报错。

classpath 的设定方法有两种:

  • 在系统环境变量中设置 classpath 环境变量,不推荐;
  • 在启动 JVM 时设置 classpath 变量,推荐。

我们强烈不推荐在系统环境变量中设置 classpath,那样会污染整个系统环境。在启动 JVM 时设置 classpath 才是推荐的做法。实际上就是给 java 命令传入 -classpath-cp 参数:

1
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello

或者使用 -cp 的简写:
1
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello

没有设置系统环境变量,也没有传入 -cp 参数,那么 JVM 默认的 classpath 为.,即当前目录:
1
java abc.xyz.Hello

上述命令告诉 JVM 只在当前目录搜索 Hello.class

在 IDE 中运行 Java 程序,IDE 自动传入的 - cp 参数是当前工程的 bin 目录和引入的 jar 包。
通常,我们在自己编写的 class 中,会引用 Java 核心库的 class,例如,StringArrayList 等。这些 class 应该上哪去找?
有很多 “如何设置 classpath” 的文章会告诉你把 JVM 自带的 rt.jar 放入 classpath,但事实上,根本不需要告诉 JVM 如何去 Java 核心库查找 class,JVM 怎么可能笨到连自己的核心库在哪都不知道?

Note: 不要把任何 Java 核心库添加到 classpath 中!JVM 根本不依赖 classpath 加载核心库!

更好的做法是,不要设置 classpath!默认的当前目录. 对于绝大多数情况都够用了。
假设我们有一个编译后的 Hello.class,它的包名是 com.example,当前目录是 C:\work,那么,目录结构必须如下:

1
2
3
4
C:\work
└─ com
└─ example
└─ Hello.class

运行这个 Hello.class 必须在当前目录下使用如下命令:
1
C:\work> java -cp . com.example.Hello

JVM 根据 classpath 设置的. 在当前目录下查找 com.example.Hello,即实际搜索文件必须位于 com/example/Hello.class。如果指定的. class 文件不存在,或者目录结构和包名对不上,均会报错。

jar 包

如果有很多 .class 文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。
jar 包就是用来干这个事的,它可以把 package 组织的目录层级,以及各个目录下的所有文件(包括. class 文件和其他文件)都打成一个 jar 文件,这样一来,无论是备份,还是发给客户,就简单多了。
jar 包实际上就是一个 zip 格式的压缩文件,而 jar 包相当于目录。如果我们要执行一个 jar 包的 class,就可以把 jar 包放到 classpath 中:

1
java -cp ./hello.jar abc.xyz.Hello

这样 JVM 会自动在 hello.jar 文件里去搜索某个类。

那么问题来了:如何创建 jar 包?
因为 jar 包就是 zip 包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择 “发送到”,“压缩 (zipped) 文件夹”,就制作了一个 zip 文件。然后,把后缀从 .zip 改为 .jar,一个 jar 包就创建成功。

假设编译输出的目录结构是这样:

1
2
3
4
5
6
7
8
9
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class

这里需要特别注意的是,jar 包里的第一层目录,不能是 bin,而应该是 hongmingmr。如果在 Windows 的资源管理器中看,应该长这样:
1
2
3
4
5
6
7
8
9
10
package_sample
└─ bin
└─ hello.zip
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class

如果长这样:
1
2
3
4
5
6
7
8
9
10
11
package_sample
└─ bin
└─ hello.zip
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class

说明打包打得有问题,JVM 仍然无法从 jar 包中查找正确的 class,原因是 hong.Person 必须按 hong/Person.class 存放,而不是 bin/hong/Person.class

jar 包还可以包含一个特殊的 /META-INF/MANIFEST.MF 文件,MANIFEST.MF 是纯文本,可以指定 Main-Class 和其它信息。JVM 会自动读取这个 MANIFEST.MF 文件,如果存在 Main-Class,我们就不必在命令行指定启动的类名,而是用更方便的命令:

1
java -jar hello.jar

jar 包还可以包含其它 jar 包,这个时候,就需要在 MANIFEST.MF 文件里配置 classpath 了。
在大型项目中,不可能手动编写 MANIFEST.MF 文件,再手动创建 zip 包。Java 社区提供了大量的开源构建工具,例如 Maven,可以非常方便地创建 jar 包。

小结

  • JVM 通过环境变量 classpath 决定搜索 class 的路径和顺序;
  • 不推荐设置系统环境变量 classpath,始终建议通过 - cp 命令传入;
  • jar 包相当于目录,可以包含很多 .class 文件,方便下载和使用;
  • MANIFEST.MF 文件可以提供 jar 包的信息,如 Main-Class,这样可以直接运行 jar 包。

补充

  • idea 软件中 .class 在根目录的 out -> production 文件下
  • 个人理解 jar 包 : 就是将目录下的类打包,供其他人使用的,可以直接调用的压缩包文件

示例

我的目录

1
2
3
4
5
6
7
8
9
IdeaTestProjects
└─class2
└─unit10
├─test1
│ └─Main.class
│ └─Person.class

└─test2
└─TestMain.class

将文件夹 IdeaTestProjects 打包 zip 文件,再将后缀名改为 jar, 然后运行如下命令
1
java -cp IdeaTestProjects.jar class2.unit10.test1.Main

输出结果为:
1
2
3
4
name: Qeuro
age: 23
name: qeuro
age: 23

模块

一个 class 类是一个 java 文件,后缀名为 .java,编译之后生成 .class 文件,而这个 .class 文件才真正可以被 JVM 执行的字节码。

定义

从 Java 9 开始,JDK 又引入了模块(Module)。
什么是模块?这要从 Java 9 之前的版本说起。
我们知道,.class 文件是 JVM 看到的最小可执行文件,而一个大型程序需要编写很多 Class,并生成一堆 .class 文件,很不便于管理,所以,jar 文件就是 .class 文件的容器。
Java 9 之前,一个大型 Java 程序会生成自己的 jar 文件,同时引用依赖的第三方 jar 文件,而 JVM 自带的 Java 标准库,实际上也是以 jar 文件形式存放的,这个文件叫 rt.jar,一共有 60 多 M。
如果是自己开发的程序,除了一个自己的 app.jar 以外,还需要一堆第三方的 jar 包,运行一个 Java 程序,一般来说,命令行写这样:

1
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main

Note: JVM 自带的标准库 rt.jar 不要写到 classpath 中,写了反而会干扰 JVM 的正常运行。

如果漏写了某个运行时需要用到的 jar,那么在运行期极有可能抛出 ClassNotFoundException
所以,jar 只是用于存放 class 的容器,它并不关心 class 之间的依赖。

从 Java 9 开始引入的模块,主要是为了解决 “依赖” 这个问题。如果 a.jar 必须依赖另一个 b.jar 才能运行,那我们应该给 a.jar 加点说明啥的,让程序在编译和运行的时候能自动定位到 b.jar,这种自带 “依赖关系” 的 class 容器就是模块。
为了表明 Java 模块化的决心,从 Java 9 开始,原有的 Java 标准库已经由一个单一巨大的 rt.jar 分拆成了几十个模块,这些模块以 .jmod 扩展名标识,可以在 $JAVA_HOME/jmods 目录下找到它们:
- java.base.jmod
- java.compiler.jmod
- java.datatransfer.jmod
- java.desktop.jmod
- ...

这些 .jmod 文件每一个都是一个模块,模块名就是文件名。例如:模块 java.base 对应的文件就是 java.base.jmod。模块之间的依赖关系已经被写入到模块内的 module-info.class 文件了。所有的模块都直接或间接地依赖 java.base 模块,只有 java.base 模块不依赖任何模块,它可以被看作是 “根模块”,好比所有的类都是从 Object 直接或间接继承而来。
把一堆 class 封装为 jar 仅仅是一个打包的过程,而把一堆 class 封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是 JNI 扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的 JVM 提供不同的版本。

编写模块

如何编写模块呢?还是以具体的例子来说。首先,创建模块和原有的创建 Java 项目是完全一样的,以 oop-module 工程为例,它的目录结构如下:

1
2
3
4
5
6
7
8
9
10
oop-module
├── bin
├── build.sh
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java

其中,bin 目录存放编译后的 .class 文件,src 目录存放源码,按包名的目录结构存放,仅仅在 src 目录下多了一个 module-info.java 这个文件,这就是模块的描述文件。在这个模块中,它长这样:
1
2
3
4
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入 java.base
requires java.xml;
}

其中,module 是关键字,后面的 hello.world 是模块的名称,它的命名规范与包一致。花括号的 requires xxx; 表示这个模块需要引用的其他模块名。除了 java.base 可以被自动引入外,这里我们引入了一个 java.xml 的模块。

当我们使用模块声明了依赖关系后,才能使用引入的模块。例如,Main.java 代码如下:

1
package com.itranswarp.sample;

// 必须引入 java.xml 模块后才能使用其中的类:
import javax.xml.XMLConstants;

public class Main {
public static void main(String[] args) {
Greeting g = new Greeting();
System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
}
}

1
2
3
4
> 如果把 `requires java.xml;` 从 `module-info.java` 中去掉,编译将报错。可见,模块的重要作用就是声明依赖关系。

> 下面,我们用 JDK 提供的命令行工具来编译并创建模块。
> 首先,我们把工作目录切换到 `oop-module`,在当前目录下编译所有的 `.java` 文件,并存放到 `bin` 目录下,命令如下:

javac -d bin src/module-info.java src/com/itranswarp/sample/*.java
1
> 如果编译成功,现在项目结构如下:

oop-module
├── bin
│ ├── com
│ │ └── itranswarp
│ │ └── sample
│ │ ├── Greeting.class
│ │ └── Main.class
│ └── module-info.class
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
1
2
3
> 注意到 `src` 目录下的 `module-info.java` 被编译到 `bin` 目录下的 `module-info.class`。

> 下一步,我们需要把 `bin` 目录下的所有 `class` 文件先打包成 `jar`,在打包的时候,注意传入 `--main-class` 参数,让这个 `jar` 包能自己定位 `main` 方法所在的类:

jar –create –file hello.jar –main-class com.itranswarp.sample.Main -C bin .
1
> 现在我们就在当前目录下得到了 `hello.jar` 这个 `jar` 包,它和普通 `jar` 包并无区别,可以直接使用命令 `java -jar hello.jar` 来运行它。但是我们的目标是创建模块,所以,继续使用 `JDK` 自带的 jmod 命令把一个 `jar` 包转换成模块:

jmod create –class-path hello.jar hello.jmod
1
2
3
4
   > 于是,在当前目录下我们又得到了 `hello.jmod` 这个模块文件,这就是最后打包出来的传说中的模块!

## 运行模块
> 要运行一个 jar,我们使用 `java -jar xxx.jar` 命令。要运行一个模块,我们只需要指定模块名。试试:

java –module-path hello.jmod –module hello.world
1
> 结果是一个错误:

Error occurred during initialization of boot layer
java.lang.module.FindException: JMOD format not supported at execution time: hello.jmod
1
> 原因是 `.jmod` 不能被放入 `--module-path` 中。换成 `.jar` 就没问题了:

$ java –module-path hello.jar –module hello.world
Hello, xml!
1
2
3
4
5
6
   > 那我们辛辛苦苦创建的 hello.jmod 有什么用?** 答案是我们可以用它来打包 JRE。**

## 打包 JRE
前面讲了,为了支持模块化,Java 9 首先带头把自己的一个巨大无比的 rt.jar 拆成了几十个. jmod 模块,原因就是,运行 Java 程序的时候,实际上我们用到的 JDK 模块,并没有那么多。不需要的模块,完全可以删除。
过去发布一个 Java 应用程序,要运行它,必须下载一个完整的 JRE,再运行 jar 包。而完整的 JRE 块头很大,有 `100+M`。怎么给 JRE 瘦身呢?
现在,JRE 自身的标准库已经分拆成了模块,只需要带上程序用到的模块,其他的模块就可以被裁剪掉。怎么裁剪 JRE 呢?并不是说把系统安装的 JRE 给删掉部分模块,而是 “复制” 一份 JRE,但只带上用到的模块。为此,JDK 提供了 `jlink` 命令来干这件事。命令如下:

jlink –module-path hello.jmod –add-modules java.base,java.xml,hello.world –output jre/
1
2
我们在 `--module-path` 参数指定了我们自己的模块 `hello.jmod`,然后,在 `--add-modules` 参数中指定了我们用到的 3 个模块 `java.base`、`java.xml` 和 `hello.world`,用 `,` 分隔。最后,在 `--output` 参数指定输出目录。
现在,在当前目录下,我们可以找到 jre 目录,这是一个完整的并且带有我们自己 `hello.jmod` 模块的 JRE。试试直接运行这个 JRE:

$ jre/bin/java –module hello.world
Hello, xml!
1
2
3
4
5
6
7
   要分发我们自己的 `Java` 应用程序,只需要把这个 `jre` 目录打个包给对方发过去,对方直接运行上述命令即可,既不用下载安装 `JDK`,也不用知道如何配置我们自己的模块,极大地方便了分发和部署。

## 访问权限
前面我们讲过,`Java` 的 `class` 访问权限分为 `public`、`protected`、`private` 和默认的包访问权限。引入模块后,这些访问权限的规则就要稍微做些调整。
确切地说,`class` 的这些访问权限只在一个模块内有效,模块和模块之间,例如,`a` 模块要访问 `b` 模块的某个 `class`,必要条件是 `b` 模块明确地导出了可以访问的包。

举个例子:我们编写的模块 `hello.world` 用到了模块 `java.xml` 的一个类 `javax.xml.XMLConstants`,我们之所以能直接使用这个类,是因为模块 `java.xml` 的 `module-info.java` 中声明了若干导出:

module java.xml {
exports java.xml;
exports javax.xml.catalog;
exports javax.xml.datatype;

}
1
只有它声明的导出的包,外部代码才被允许访问。换句话说,如果外部代码想要访问我们的 `hello.world` 模块中的 `com.itranswarp.sample.Greeting` 类,我们必须将其导出:

module hello.world {
exports com.itranswarp.sample;

  requires java.base;
  requires java.xml;

}

1
因此,模块进一步隔离了代码的访问权限。

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@attention           注意
@author 作者
@bug 缺陷,链接到所有缺陷汇总的缺陷列表
@brief 简要注释
@code 代码块开始,与 “endcode” 成对使用
@endcode 代码块结束,与 “code” 成对使用
@details 详细注释
@date 日期
@file <文件名> 文件参考,用在文件注释中
@param 参数,用在函数注释中
@return 返回,用在函数注释中
@todo TODO,链接到所有 TODO 汇总的 TODO 列表
@version 版本
@warning 警告

Vscode 用户自定义片段

个人设置

1
2
3
4
5
6
7
8
9
10
11
"cppDocuNote": {
"prefix": "docuNote",
"body": [
"/**",
"* $1",
"* @param: $2",
"* @return: $3",
"*/$0",
],
"description": "Documentation notes"
}

实例

1
2
3
4
5
6
7
/**
*〈一句话功能简述〉
*〈功能详细描述〉
* @param [参数 1] [参数 1 说明]
* @param [参数 2] [参数 2 说明]
* @return [返回类型说明]
*/

  1. 程序设计返回结果

    1. Accepted: 通过
    2. Wrong Answer: 答案错误
    3. Runtime Error: 表示程序因为非法访问或未处理异常而结束。
    4. Memory Limit Exceeded: 表示程序因为使用的内存超过规定的内存限制。
    5. Presentation Error: 表示虽然程序输出的答案是对的,但是换行或空格等不符合输出格式要求
    6. Time Limit Exceed: 超时
    7. Output Limit Exceeded: 表示程序输出了过多的内容。
    8. Compile Error: 表示所提交的源代码没能通过编译,不符合语法规定。
    9. System Error, Validator Error: 表示系统发生错误无法正常判题。
    10. Segmentation Fault: 段错误:访问的内存超过了系统所给这个程序的内存空间
  2. 状态

    例子:找最短路径的问题中,状态仅仅是目前所在位置的坐标。

    当状态更加复杂是,就需要封装成一个类来表示转态了。

  3. 状态转移、转移的方式

    例子:同1中的例子,转移的方式为4个方向移动

    又如:八连通($P_{32}$),8个方向共对应了8种状态转移。

  4. next_permutation 函数使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <iostream>
    #include <algorithm>
    using namespace std;
    int main()
    {
    int a[4] = {1, 5, 4, 3}, i;
    sort(a, a + 4); //在使用全排列函数之前,一定要先将数组中的数排序哦(不排序好像也可以)
    do {
    for (i = 0; i < 4; i++) cout << a[i] << " ";
    cout << endl;
    } while (next_permutation(a, a + 3));

    for (i = 0; i < 4; i++) cout << a[i] << " ";
    return 0;
    }

    使用 next_permutation 后,原数组的不变。

  5. 贪心算法

    贪心算法就是遵循某种规则,不断贪心地选取当期那最优策略的算法设计方法。

    如果我们不慎重地选择一个正确的规则,就会得到错误的算法。

    如果问题能够用贪心算法来求解的话,那么它通常是非常高效的。

    1. 区间问题:在可选工作中,每次都选择结束时间最早的工作。
    2. 字典序比较类型的问题经常用得上贪心法
  6. 动态规划

    1. 记忆化搜索

      在需要剪枝的情况下,可能会把各种参数都写在函数上,但是这种情况下会让记忆化搜索难以实现,需要注意

    2. def

      一步步按顺序求出问题的解的方法。

  7. memset 初始化数组

    语法: memset(<数组名>, <初始化的值>, <数组的大小>)

    虽然 memset 按照1字节为单位对内存进行填充,-1的每一位二进制位都为1,所以不可以像0一样用 memset 进行初始化。通过使用 Memset 可以快速地对高维数组等进行初始化,但是需要注意无法初始化成1之类的数值。

  8. 移位运算

    1. $1 << n = 2^n$
    2. $n << 1 = 2n$
    3. $n >> 1 = \lfloor{\frac{n}{2.0}}\rfloor$
  9. $a^b$ 算法

    求a的b次方对p取模的值,其中 $1 \leq a, b, p \leq 10^9$

    1
    2
    3
    4
    5
    6
    7
    8
    int power(int a, int b, int p) {
    int ans = 1 % p;
    for (; b; b >>= 1) {
    if (b & 1) ans = 1ll *ans * a % p;
    a = 1ll * a * a % p;
    }
    return ans;
    }
  10. $a * b % p$

    求a乘b对p取模的值,其中 $1 \leq a, b, p \leq 10^{18}$

    1. 方法一
      1
      2
      3
      4
      5
      6
      7
      8
      long long mul(long long a, long long b, long long p) {
      long long ans = 0;
      for (; b; b >>= 1) {
      if (b & 1) ans = (ans + a) % p;
      a = a * 2 % p;
      }
      return ans;
      }
    2. 方法二 ($P_6$)
      1
      2
      3
      4
      long long mul(long long a, long long b, long long p) {

      }

  11. 二进制状态压缩

    操作 运算
    取出整数n在二进制表示下的第k位 (n >> k) & 1
    取出整数n在二进制表示下的第0 ~ k - 1位(后k位) n & ((1 << k) - 1)
    把整数n在二进制表示下的第k位取反 n xor (1 << k)
    对整数n在二进制表示下的第k位赋值为1 n | (1<<k)
    对整数n在二进制表示下的第k为赋值为0 n & (~(1 << k))

插件列表

插件名 用途
C/C++
Chinese (Simplified)
Code Runner
highlight-words
indent-rainbow
IntelliCode
IntelliCode API Usage Examples
LaTeX Workshop
One Dark Pro
Path Intellisense
Power Mode
Python
Remote - SSH
Vim
vscode-icons

卸载

1、退出vscode

2、删除配置文件

1
2
3
sudo rm -rf $HOME/Library/Application\ Support/Code
# if you're using insider*
sudo rm -rf $HOME/Library/Application\ Support/Code\ -\ Insiders/

或者

1
2
3
4
5
6
7
sudo rm -rf $HOME/Library/Preferences/com.microsoft.VSCode.helper.plist
sudo rm -rf $HOME/Library/Preferences/com.microsoft.VSCode.plist
sudo rm -rf $HOME/Library/Caches/com.microsoft.VSCode
sudo rm -rf $HOME/Library/Caches/com.microsoft.VSCode.ShipIt/
sudo rm -rf $HOME/Library/Application\ Support/Code/
sudo rm -rf $HOME/Library/Saved\ Application\ State/com.microsoft.VSCode.savedState/
sudo rm -rf $HOME/.vscode/

3、删除扩展插件

1
2
3
sudo rm -rf $HOME/.vscode
# if you're using insider*
sudo rm -rf $HOME/.vscode-insiders/

4、从应用中删除vscode(如果存在的话,如果直接解压后使用似乎不用做)

5、卸载所有vscode系统配置

6、重新下载vscode安装

1
sudo rm -rf $HOME/.vscode

删除云端配置

设置代码模板:自动生成文件名、作者、创建日期等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"pyheader": {
"prefix": "test",
"body": [
"/**",
"* @author: test",
"* @desc:$1",
"* @date: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}",
"* @Email:test@qq.com",
"* @url:test.com",
"*/",
"",
""
],
"description": "test"
}
}
  1. 打开 vs­code 在左下角设置图标 -> 用户代码片段
  2. For_Loop: 当前 snip­pet 名字。
  3. pre­fix: 前缀,代码块使用快捷方式;键入前缀,按 tab 键,代码块就会被使用。
  4. body: 代码块内容;换行使用 rn。
  5. de­scrip­tion: 键入前缀,vs­code 感知到前缀,显示的说明内容。
  6. $1,$2,$0: 指定代码模块生成后,编辑光标出现位置;使用 Tab 键进行切换 (编辑光标按 $1,$2,$3...$0 的顺序跳转),$0 是光标最后可切换位置。

Snippet 语法

Tabstops

$1,$2 指定代码块生成后,光标出现的位置;不同位置的相同 $1 位置同时出现光标。

Placeholders

给光标出现位置加上默认值;例如,${1:an­other\${2:place­holder}}$1 处位置默认值是 an­other

Choice

光标位置设置多个值可供选择;例如,${1|one,two,three|}$1 位置处可以选择 one,two,three 中一个词填充在此处。

Variables

  1. 常用变量

    • TM_SELECTED_TEXT 当前选中内容或空字符串
    • TM_CURRENT_LINE 当前行内容
    • TM_CURRENT_WORD 光标处字符或空字符串
    • TM_LINE_INDEX 从 0 开始的行号
    • TM_LINE_NUMBER 从 1 开始的行号
    • TM_FILENAME 当前被编辑文档名
    • TM_FILENAME_BASE 当前被编辑文档名,没有后缀
    • TM_DIRECTORY 当前被编辑文档目录
    • TM_FILEPATH 当前被编辑文档全路径
    • CLIPBOARD 当前剪切板内容
    • WORKSPACE_NAME 打开的工作空间或文件夹的名称
  2. 日期和时间相关变量

    • CURRENT_YEAR 当前年
    • CURRENT_YEAR_SHORT 当前年后两位
    • CURRENT_MONTH 月份,两位数字表示,例如 02
    • CURRENT_MONTH_NAME 月份全称,例如’July’
    • CURRENT_MONTH_NAME_SHORT 月份简写 ,例如’Jul
    • CURRENT_DATE 某天
    • CURRENT_DAY_NAME 星期几, 例如’Monday’
    • CURRENT_DAY_NAME_SHORT 星期几的简写, ‘Mon’
    • CURRENT_HOUR 小时,24 小时制
    • CURRENT_MINUTE 分钟
    • CURRENT_SECOND 秒数
    • CURRENT_SECONDS_UNIXUnix 时代以来的秒数

VsCode-Terminal 设置

部分设置

1
2
3
4
"workbench.colorCustomizations" : {
"terminal.foreground" : "#00FD61",
"terminal.background" : "#383737"
}

完整设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"workbench.colorCustomizations": {
"terminal.background": "#181818",
"terminal.foreground": "#D8D8D8",
"terminalCursor.background": "#D8D8D8",
"terminalCursor.foreground": "#D8D8D8",
"terminal.ansiBlack": "#181818",
"terminal.ansiBlue": "#7CAFC2",
"terminal.ansiBrightBlack": "#585858",
"terminal.ansiBrightBlue": "#7CAFC2",
"terminal.ansiBrightCyan": "#86C1B9",
"terminal.ansiBrightGreen": "#A1B56C",
"terminal.ansiBrightMagenta": "#BA8BAF",
"terminal.ansiBrightRed": "#AB4642",
"terminal.ansiBrightWhite": "#F8F8F8",
"terminal.ansiBrightYellow": "#F7CA88",
"terminal.ansiCyan": "#86C1B9",
"terminal.ansiGreen": "#A1B56C",
"terminal.ansiMagenta": "#BA8BAF",
"terminal.ansiRed": "#AB4642",
"terminal.ansiWhite": "#D8D8D8",
"terminal.ansiYellow": "#F7CA88"
}

用户配置

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
{
//{{{> begin

/// workplace
// file
// 自动保存选项
"files.autoSave": "onWindowChange",
"files.autoGuessEncoding": true,
// editor
"editor.fontSize": 15.5,
"editor.suggestSelection": "recentlyUsedByPrefix",
"editor.inlayHints.fontFamily": "JetBrainsMono Nerd Font, JetBrains Mono, MesloLGS NF, Menlo, Monaco, Courier New, monospace",
"editor.snippetSuggestions": "inline",
"editor.fontFamily": "JetBrainsMono Nerd Font, JetBrains Mono, MesloLGS NF, Menlo, Monaco, Courier New, monospace, Menlo for Powerline",
"editor.wordWrap": "on",
"editor.quickSuggestionsDelay": 10,
// 自动修正格式
"editor.formatOnType": false,
"editor.cursorWidth": 4,
"diffEditor.wordWrap": "on",
// workbench
"workbench.colorTheme": "One Dark Pro",
"workbench.iconTheme": "vscode-icons",
"workbench.sideBar.location": "right",
"workbench.startupEditor": "none",
"workbench.colorCustomizations": {
"editorCursor.foreground": "#ff6347",
"terminalCursor.foreground": "#ff6347"
},
// 资源管理器排列方式
"explorer.sortOrder": "foldersNestsFiles",
// update 发行说明
"update.showReleaseNotes": false,
// 关闭工作区信任
"security.workspace.trust.enabled": false,
// 在文件和文件夹上显示错误和警告
"problems.decorations.enabled": false,
// 选项卡是否应在多行之间换行
"workbench.editor.wrapTabs": true,

/// abundant
// "editor.formatOnPaste": true, // 粘贴自动修正格式
// "files.trimTrailingWhitespace": true, // 自动删除尾随空格
// "files.trimFinalNewlines": true, // 自动删除空行
// "workbench.editor.decorations.colors":false,

/// terminal
"terminal.integrated.inheritEnv": true,
"terminal.integrated.fontSize": 16,
"terminal.integrated.fontFamily": "JetBrains Nerd Font, DroidSansMono Nerd Font, MesloLGS NF",
"terminal.integrated.cursorWidth": 4,

/// powermode
"powermode.enabled": true,
"powermode.presets": "particles",
"powermode.shake.enabled": false,
"powermode.combo.counterEnabled": "hide",
"powermode.combo.timerEnabled": "hide",

/// python
// "python.defaultInterpreterPath": "~/anaconda3/bin/python",
"python.languageServer": "Default",
"python.defaultInterpreterPath": "~/anaconda3/bin/python",
// "python.linting.enabled": false,

/// cpp
"C_Cpp.errorSquiggles": "disabled",
"C_Cpp.intelliSenseEngine": "default",
"C_Cpp.intelliSenseUpdateDelay": 500,
"C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, UseTab: Never, IndentWidth: 4, TabWidth: 4, BreakBeforeBraces: Attach, AllowShortIfStatementsOnASingleLine: false, IndentCaseLabels: false, ColumnLimit: 80, AccessModifierOffset: -4 }",
"C_Cpp.suggestSnippets": false,

/// leetcode
"settingsSync.ignoredExtensions": [
"leetcode.vscode-leetcode"
],

/// Doxygen
"doxdocgen.generic.authorName": "<username>",
"doxdocgen.generic.authorEmail": "<usermail>",
"doxdocgen.generic.order": [
"brief",
"empty",
"tparam",
"param",
"return",
],
"doxdocgen.file.versionTag": "@version 1.0",
"doxdocgen.generic.dateFormat": "YYYY-MM-DD HH:MM:SS",

/// git
// "git.ignoreMissingGitWarning": true,

/// vim
"vim.useSystemClipboard": true,
"vim.replaceWithRegister": true,
// "vim.visualstar": true,
"vim.mouseSelectionGoesIntoVisualMode": false,
"editor.cursorBlinking": "solid",
// Use key mappings from a `.vimrc` file.
"vim.vimrc.enable": true,
// Path to a Vim configuration file. If unset, it will check for `$HOME/.vscodevimrc`, `$HOME/.vimrc`, `$HOME/_vimrc`, and `$HOME/.config/nvim/init.vim`, in that order.
"vim.vimrc.path": "~/.vimrc",

/// incedent-rainbow
// Using the light mode
"indentRainbow.indicatorStyle": "light",
// we use a simple 1 pixel wide line
"indentRainbow.lightIndicatorStyleLineWidth": 2,
// the same colors as above but more visible
"indentRainbow.colors": [
"rgba(255,255,64,0.3)",
"rgba(127,255,127,0.3)",
"rgba(255,127,255,0.3)",
"rgba(79,236,236,0.3)"
],

/// latex
// 使用内置pdf浏览器预览pdf文件
"latex-workshop.view.pdf.viewer": "tab",
// tex文件时禁止自动编译
"latex-workshop.latex.autoBuild.run": "never",
// 隐藏在编译出现错误或警告时自动弹出的提示窗口
"latex-workshop.message.error.show": false,
"latex-workshop.message.warning.show": false,
// 启用或禁用自动提示功能
"latex-workshop.intellisense.package.enabled": true,
// 启用或禁用数学符号的自动提示功能。
"latex-workshop.intellisense.unimathsymbols.enabled": true,
// 配置自动提示的更新延迟时间
"latex-workshop.intellisense.update.delay": 0,
"latex-workshop.latex.recipe.default": "lastUsed",
// 自动清理
"latex-workshop.latex.autoClean.run": "onBuilt",
// 发行版的可执行文件路径
// "latex-workshop.latex.path":
// 将编译工具配置添加到用户设置中
"latex-workshop.latex.tools": [
{
"name": "latexmk",
"command": "latexmk",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"-pdf",
"%DOC%"
]
},
{
"name": "pdflatex",
"command": "pdflatex",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"%DOC%"
]
},
{
"name": "xelatex",
"command": "xelatex",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"%DOC%"
]
},
{
"name": "bibtex",
"command": "bibtex",
"args": [
"%DOCFILE%"
]
}
],
// 编译方案配置
"latex-workshop.latex.recipes": [
{
"name": "XeLaTeX",
"tools": [
"xelatex"
]
},
{
"name": "latexmk",
"tools": [
"latexmk"
]
},
{
"name": "PDFLaTeX",
"tools": [
"pdflatex"
]
},
{
"name": "BibTeX",
"tools": [
"bibtex"
]
},
{
"name": "pdflatex -> bibtex -> pdflatex*2",
"tools": [
"pdflatex",
"bibtex",
"pdflatex",
"pdflatex"
]
},
{
"name": "xelatex -> bibtex -> xelatex*2",
"tools": [
"xelatex",
"bibtex",
"xelatex",
"xelatex"
]
},
{
"name": "xelatex*2",
"tools": [
"xelatex",
"xelatex"
]
}
],
// 编译后自动清理多余文件
"latex-workshop.latex.clean.fileTypes": [
"*.aux",
"*.bbl",
"*.blg",
"*.idx",
"*.ind",
"*.lof",
"*.lot",
"*.out",
"*.toc",
"*.acn",
"*.acr",
"*.alg",
"*.glg",
"*.glo",
"*.gls",
"*.ist",
"*.fls",
"*.log",
"*.fdb_latexmk",
"*.pdfsync"
],

// "vsicons.dontShowNewVersionMessage": true,

//<}}} end

}

理解

网上的解释是 由ubuntu系统自动创建的回收站文件夹,每个盘都有一个 。s
也就是说,系统在删除某个盘里的垃圾文件的时候,是创建一个 .Trash-1000文件夹,然后将它放进去。

例子

比如 我在u盘上执行了删除操作,ubuntu系统会在u盘里也创建一个.Trash-1000文件夹,info文件夹应该是保存的是垃圾文件的对应关系,files文件夹里应该是删除的垃圾文件。

管理

如果在 ubuntu系统里–清除回收站,各个盘里对应的垃圾文件就会自动删除了。

  1. 新建 shadowsocks.desktop

  2. 将以下内容根据自己的情况更改,复制到 shadowsocks.desktop 内:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [Desktop Entry]
    Version=1.0
    Type=Application
    Name=Shadowsocks
    Comment=Connection Manager — Shadowsocks-Qt5
    Exec=/home/gph/shadowsocks_ubuntu/Shadowsocks-Qt5-3.0.1-x86_64.AppImage // AppImage程序路径
    Icon=/home/gph/shadowsocks_ubuntu/shadowsocks.png // 小飞机的图标
    Terminal=false
    StartupNotify=true
    Categories=Application
  3. 即创建快捷方式

Note: 如果没有创建快捷方式,好像应该不能开机自启。

PE

微PE工具箱

步骤

  1. 使用U盘启动快捷项,启动PE
  2. 在分区大师工具看下自己ESP在哪个盘符,就在UEFI引导修复里面选择哪个盘符。
    ./105/10.png)
  3. 开UEFI引导修复,在ESP里面选择H盘符(若没有则点击 挂载
    ./105/11.png)
  4. 选择根目录:windows目录(例:C:\Windows
  5. 点击开始修复,点击确定
  6. 最后重新开机,就可以进入系统了

准备工作

步骤

  1. 右击系统镜像 - 挂载

  2. 将下载的WTG解压出来,然后运行

  3. 按照下图进行设置

    • 点击 浏览... ,选择挂载的 系统镜像的目录 -> sources -> install.wim
    • 选择安装的系统的U盘,即 浏览... 下面一栏(左面第二栏)
    • 再下面一栏(左面第三栏),选择要安装系统的版本(推荐:专业版或企业版)
    • 高级选项(右面)- 常用:选择: UEFI+MBR,其他的根据自己的需求来选择
  4. 然后,使用U盘快捷启动进入系统就可以了

**【Note】:**如果原来系统的打不开了,即引导出了问题,见下一篇文章(UEFI引导修复)

0%