概述和环境

源码文件

  1. 确保安装了JDK, 并且配置了环境变量
  2. jdk/lib目录中可以找到src.zip, 这个压缩包包含了所有的公共类库的源码
  3. 使用jar xvf jdk/lib/src.zip可以将src.zip解压缩到当前目录
  4. 如果是别的源码, 比如编译器, 原生方法, 虚拟机, 私有辅助类, 需要从openjdk那边拿到

控制台运行Java

  1. 一个包含Main函数的Java类, 使用javac xxx.java可以将xxx.java文件编译为.class文件
  2. 然后再使用java xxx, 不需要加.class后缀即可直接运行这个编译后的文件

JShell

  • JDK 9引入了另一种使用Java的方法,就是"读取-评估-打印循环" (Read-Evaluate-Print Loop, REPL)
  • 输入一个表达式, JShell会评估输入, 打印结果, 并等待下一个输入. 直接在控制台输入jshell即可开始使用
    • 输入"java".length()就会返回字符个数$1 ==> 4
    • 然后再输入4 * $1 + 1, 会返回$2 ==> 17
    • 并不需要手动输入System.out.println()也可以直接返回结果, 并自动存储变量
    • 也可以手动指定变量名, 例如int ans = 42, 会返回结果ans ==> 42

Java规范

  1. 类名需要驼峰: 首字母大写, 后面每个单词的首字母均大写
  2. 文件名必须和文件内的公共类相同
  3. main函数一定需要在public类中, 虚拟机从main函数开始执行. main方法总是静态的
  4. main函数返回值为0, 如果需要以其他的返回值返回, 需要使用System.exit(nums)
  5. 变量的声明尽可能靠近使用变量的地方
  6. JDK 10开始, 对于局部变量, 可以使用var关键字声明, 这样可以从变量的初始值推断出他的类型
    • var d = 12; // d is an int
    • var s = "12"; // s is a String
  7. Java中不区分变量的声明和定义

类型

  • Java具有8种基本类型, 其中4种整型, 2种浮点类型, 1种字符类型char(用于表示Unicode编码), 1种boolean类型
  • 同时Java具有一个表示任意精度的算数包, 大数big number是一个Java对象, 而不是基本Java类型

整型

  • Javaint无论在什么环境下都是4 Bytes, 可以表示2×1092 \times 10^9
  • 可以给数字加上下划线, 在编译时会自动去除, 方便源码阅读: 12_000_000_000
  • Java中没有无符号类型, 如果确实需要无符号数, 比如short表示范围需要在0~255之间的话, 进行计算的时候可以使用Byte.toUnsignedInt(b)来得到一个无符号整数

浮点型

  • 一般都使用8 Bytesdouble类型, 而不是4 Bytesfloat类型
  • 可以使用十六进制表示浮点数字面量, 例如0.125=230.125 = 2^{-3}可以写成0x1.0p-3
    • 这里0x表示十六进制, p表示指数. 不是e是因为e在十六进制中了, -3表示十进制的-3次方, 基数为2
  • 浮点数溢出的三种情况:正溢出,负溢出和NaN,如果正数/0就是正溢出,反之负溢出;如果0/00/0就是NaN
  • Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN表示三个特殊的值, 基本不用
  • if (x == Double.NaN)永远为假, 因为NaN是永远不会和其他值相同, (Double.NaN也不会和Double.NaN相同)
    • 如果需要判断x是否为NaN, 可以使用if (Double.isNaN(x))
  • 如果需要精确表示浮点数的话, 需要使用BigDecimal
  • 整数除0产生异常, 浮点数除0得到结果NaN

字符类型

  • char类型可以表示十六进制值, 范围从\u0000~\uFFFF
  • 需要注意注释中尽量不要出现Unicode字符, 例如 // \u000A is a newline.这句话会产生一个语法错误, 因为读程序时会将\u000A转换为一个换行符
    • // look inside c:\user这里也是会报错的, 因为\u后面没有跟着4位十六进制数
  • 原本Unicode字符不超过65536个, 所以Java设置了char类型只有16
  • 后来字符放不下了, Java设置码点来解决问题, 指一个编码表中某个字符对应的代码值
  • 码点采用十六进制编写, 加上前缀U+, 比如U+0041就是A的码点
  • Unicode的码点可以分为17个代码平面, 第一个代码平面被称为基本多语言平面, 包括了码点范围是U+0000~U+FFFF
  • 其余16个平面的码点范围是U+10000~U+10FFFF
  • 不要Java代码中使用char类型, 一般将字符串作为抽象数据类型处理

布尔类型

  • Java的整型和布尔值不能相互转换

常量

  • 使用final关键字定义的, 只能被赋值一次, 不能再更改, 一般用全大写命名
  • 可能需要创建一个常量在类中的多个方法中使用, 称为类常量, 可以使用static final设置一个类常量
  • constJava保留的一个关键字, 但是目前并没有使用
1
2
3
4
5
6
7
8
public class A {
public static final int b = 100;
// b可以被类A中的所有方法访问, 因为使用static final修饰
// 同时因为 b 是 public 的, 所以也可以被其他类使用A.b访问
public static void main(String[] args) {
final int a = 10; // a只能被main函数访问
}
}

枚举类型

1
2
enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};
Size s = Size.SMALL; // s只能存储枚举中的值, 或者是null

循环移位

  • >>表示左移, 使用符号位填充; >>>表示左移, 永远使用0填充
  • <<表示右移; 不存在<<<
  • 所有移位的右操作数都需要对32取模, 如果左操作数是Long类型, 则右操作数需要对64取模
    1
    2
    3
    4
    5
    int i = 1;
    long j = 1;
    1 << 35; // 与 1 << 3等价, 结果为8
    i << 35; // 与 1 << 3等价, 结果为8
    j << 35; // 与 1 << 35等价, 结果为34359738368

字符串

  • Java中字符串就是Unicode序列, 比如"Java\u2122"由五个Unicode字符组成
  • Java没有内置的字符串类型, 标准Java类库中提供了一个预定义类
  • 任何Java对象都可以转换为字符串, 所以"PG" + 12 = "PG12"
  • 如果多个字符串使用界定符分割的话, 可以使用静态join方法
    1
    String all = String.join("/", "S", "M", "I"); // all = "S/M/I"
  • JDK 11中提供了repeat方法
    1
    String rep = "J".repeat(3); // rep = "JJJ"
  • Java中字符串不可变, 如果需要修改一个字符串的一部分, 需要先使用substring提取字符串不需要修改的部分, 然后加上其他修改后的结果字符串
  • 字符串不可变每次都需要生成新的字符串, 会降低效率, 但是编译器底层可以实现字符串共享
  • 开发者认为字符串共享带来的收益比修改字符串带来的收益明显, 因为往往需要比对字符串是否相同, 修改频率较少
  • 只会共享字符串字面量, 由+或者substring得到的字符串无法共享
  • 因此比较两个字符串相等的时候务必使用"A".equals("A"), 如果使用==, 则会比较两个字符串引用是否相等
  • 如果要检查字符串不是空串也不是null的话, 需要先检查null:
    • if (s != null && s.length() != 0), 否则如果字符串为null的话, 调用这个字符串的length()函数会报错
  • Java中最常用的Unicode由一个代码单元表示, 但是辅助字符需要两个代码单元, 所以如果使用charAt(), 会返回指定索引的代码单元

    • 如果正好某个索引需要两个代码单元, 但是使用charAt()指定了前一个索引, 就会出现问题. 因此,charAt()一般不要用
    • 为了测试, 需要提前设置cmd窗口内编码格式为Unicode, 在cmd内输入chcp 65001将当前窗口的编码格式切换为Unicode
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public static void main(String[] args) {
      String s = "123\uD835\uDD46";
      System.out.println("当前字符串是: " + s);
      System.out.println("s.length() = " + s.length());

      System.out.println("使用s.charAt()打印每一个元素: ");
      for (int i = 0; i < s.length(); i ++) {
      System.out.print(s.charAt(i) + " ");
      }
      System.out.println();

      System.out.print("s的实际长度(码点个数)为: ");
      int trueLen = s.codePointCount(0, s.length());
      System.out.println(trueLen);

      System.out.println("使用s.codePointAt()打印每一个元素: ");
      for (int i = 0; i < trueLen; i ++) {
      System.out.print(s.codePointAt(s.offsetByCodePoints(0, i)) + " ");
      }
      }
      • 上面代码的输出结果为:
        codepoint
  • 如果需要拼接多个较短的字符串, 可以使用StringBuilder

    • StringBuffer效率不如StringBuilder, 不过StringBuffer可以支持多线程添加删除字符
    • 如果所有操作都在单线程, 则使用StringBuilder
  • JDK 15中存在文本块, 以三个引号开头结尾, 可以更加方便的写换行, 例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    String a = 
    """
    Hello
    World
    """;
    String b = "Hello\nWorld\n";
    // a == b
    // 文本块最适合放SQL语句或者HTML语句, 但是如果所有的反斜线都需要转义
    // 如果不转义就会变成不换行
    String c =
    """
    Hello\
    World
    """;
    String d = "HelloWorld\n";
    // c == d

输入输出

  • 如果是密码相关的内容, 建议不要使用println(), 可以使用Console.readPassword()
  • printf("%+f", d) 可以打印d的正负号
  • printf(%,f, d) 可以对d增加三个数一组的分割符
  • printf("%1$d,%1$x", d) 分别以十进制和十六进制打印第一个参数d
  • printf("%d%<x", d) 分别以十进制和十六进制打印同一个数
  • 使用String.format方法可以格式化字符串, 不打印输出
    1
    2
    3
    String message = String.format("Hello, %s. Next year, you'll be %d", name, age + 1);
    // JDK 15版本以后可以使用下面这种更加简单的方法
    String message = "Hello, %s. Next year, you'll be %d".formatted(name, age + 1);
  • 写入文件使用
    1
    2
    3
    4
    5
    PrintWriter out = new PrintWriter("tmp.txt", StandardCharsets.UTF_8);
    out.write("werwr");
    out.flush();
    // 如果tmp.txt不存在, 则会自动创建一个
    // 写入以后需要使用flush才能保存

控制流程

  • Java中两个嵌套的块不能重复定义相同的变量
  • for循环中不能检测了两个浮点数是否相等, 因为误差的存在, 可能会导致死循环

switch-case

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
// 第一种
switch (s) {
case "yes", "y" -> {
...
}
case "no", "n" -> {
...
}
default -> {
...
}
}

// 第二种
switch (s) {
case "y":
...
break;
case "n":
...
break;
default:
...
break;
}
// 第二种方法如果不使用break会导致执行多个分支
// 使用javac -Xlint:fallthrough xxx.java编译可以得到这个方面的警告
// 但是如果本身就想执行多个分支, 可以在整个函数外加上
// @SuppressWarnings("fallthrough"), 这样就不会产生警告了
  • 不能混用:->, 同时->存在直通的行为
  • yield也会终止switch语句, 但是还会生成一个值
    • switch表达式的关键是生成一个值, 或者抛出异常, 不允许使用return跳出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int ans = switch(s) {
case "y" -> {
...
yield 1;
}
case "n" -> {
...
yield 2;
}
default -> {
...
yield 3;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int nums = switch(s) {
case "Spring", "Summer", "Winter" -> 6
case "Fall" -> 4
default -> -1
}
// 上面表达式优于下面的表达式
switch(s) {
case "Spring", "Summer", "Winter" -> {
nums = 6;
}
case "Fall" -> {
nums = 4;
}
default -> {
nums = -1;
}
}
// 除非是需要直通式, 才会使用`break`和`yield`

大数

1
2
3
4
// 使用valueOf()静态方法可以将一个普通的数转换为大数
BigInteger a = BigInteger.valueOf(123);
// 对于一个很长的数, 使用带有字符串参数的构造器生成
BigInteger b = new BigInteger("1231231231231231231231231");
  • 对于 BigDecimal类, 总应该使用带有字符串参数的构造器生成. 尽管有BigDecimal(double), 但是传入的时候会产生浮点数精度误差
  • Java不能通过编程实现运算符的重载, 所以只能使用add(), multiply()等方法实现加减乘除
  • Java设计者只重载了+来实现字符串拼接

数组

  • Java允许长度为0的数组存在
  • 数组创建时, 数字数组初始化为0, 对象数组初始化为null, boolean数组初始化为false
  • 如果想要打印数组a, 可以直接写Arrays.toString(a), 这个返回值包含了数组中所有元素的字符串
  • 如果想要快速打印一个二维数组, 可以使用Arrays.deepToString(a)
1
2
int[] a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(Arrays.toString(a));

数组拷贝

1
2
3
4
5
6
7
8
int[] a = {1, 2, 3};
int[] b = a;
// b和a引用同一个数组
// 如果需要拷贝一个副本到新的数组, 可以使用copyOf
int[] c = Arrays.copyOf(a, a.length);
// 第二个参数是新数组的长度, 一般用于扩充新数组
// 如果需要扩充, 则数组数组自动填充0, boolean数组自动填充false
// 如果新数组长度更小, 则只拷贝前面的部分
  • main函数的参数, 是String[] args, 接受一个字符串数组, 也就是命令行指定的参数
  • java Message -h hello 这里面args[0]=-h args[1] = hello