跳转至

Basic Java⚓︎

6309 个字 342 行代码 预计阅读时间 36 分钟

Elementary Programming⚓︎

Identifiers⚓︎

标识符(identifiers) 是一个包括字母、数字、下划线 (underscores)_)和美元符号($)的字符序列。

  • 不能以数字开头
  • 不能是保留字,truefalsenull 也不行
  • 可以使任意长度
  • 区分大小写

Variables⚓︎

变量的声明、赋值和初始化语法和 C/C++ 基本一样。但不同之处在于

  • Java 不区分变量的声明和定义
  • Java 不会对方法内的局部变量赋予一个默认值,而使用未初始化的变量会发生编译错误

final⚓︎

final 的三种用法:

  • 修饰变量

    • final 变量是不可改变的,但它的值可以在运行时初始化,也可以在编译时初始化,甚至可以放在构造函数中初始化,而不必在声明的时候初始化,所以下面的语句均合法:

      final int i = 1;                            // 编译时
      final int i2 = (int)(Math.Random() * 10);   // 运行时
      final int i3;                               // 构造函数里再初始化
      
    • 如果修饰类对象,表示这个变量不可再赋成其它对象,而不是表示这个对象不可更改

      final Value v = new Value(); 
      v = new Value();  // 不允许!
      
  • 修饰方法:表示一个函数不可更改,也就是不能被重载了,而不是修饰返回值的

  • 修饰:表示整个类不能被继承了(自然里面的所有方法也相当于被加了 final

注:Java 有保留字 const,但是没有任何实际功能。

Numerical Data Types⚓︎

数值基本类型:

类型 大小 最小值 最大值
byte 1B -128 127
short 2B -32768 32767
int 4B –2,147,483,648 2,147,483,647
long 8B –9,223,372,036,854,775,808 9,223,372,036,854,775,807
float 4B 大约 –3.4E+38,有 7 个有效位 大约 3.4E+38,有 7 个有效位
double 8B 大约 –1.7E+308,有 15 个有效位 1.7E+308,有 15 个有效位

Java 中整型的范围运行 Java 代码的机器无关,解决了平台移植的问题。

读取数字:

nextByte()      // reads an integer of the byte type
nextShort()     // reads an integer of the short type
nextInt()       // reads an integer of the int type
nextLong()      // reads an integer of the long type
nextFloat()     // reads a number of the float type
nextDouble()    // reads a number of the double type

数值运算符和 C/C++ 基本一致,故不展开介绍。

特殊的数字格式:

145_345.23  // 可以在数字里的任何地方放下划线
0b11001011  // 二进制
// 0x:十六进制;0:八进制

Literals⚓︎

  • 整数字面量
    • 只能赋给整数变量
    • 当字面量太大,变量无法容纳时将会发生编译错误
    • 默认为 int 类型;若要指定为 long 类型,需要在附加一个 Ll 后缀
  • 浮点数字面量
    • 默认为 double 类型
    • 若要指定为 float 类型,需要在附加一个 Ff 后缀;或者附加一个 Dd 后缀来显式指定 double
    • 特殊的浮点数值:
      • 正无穷大:Double.POSITIVE_INFINITY
      • 负无穷大:Double.NEGATIVE_INFINITY
      • NaN(不是一个数字Double.NaN
        • Double.isNaN 来判断是否是数字

Type Casting⚓︎

实线表示无信息丢失的转换,虚线表示可能有精度损失的转换。

类型转换(type casting) 分为:

  • 隐式(implicit) 转换:
    • 类型拓宽:double d = 3
  • 显式(explicit) 转换:
    • 类型缩窄:int i = (int)3.0;
    • 小数部分截断:int i = (int)3.9;

四舍五入:

double x = 9.997;
int nx = (int) Math.round(x);   // 因为 round 返回值为 long,所以还要转换到 int 上

Selections⚓︎

布尔类型:

  • 关键字为 boolean,取值只有 truefalse
  • 整型与布尔值之间不能相互进行转换
    • 所以当 evenboolean 变量时,if (even != 0) 是错的

if-else 语句:

  • 基本上和 C/C++ 语法一致
  • else 自己匹配相同块内最近的 if 语句

    例子

switch-case 语句:

  • 基本上和 C/C++ 语法一致
  • switch 语句必须得到 char / byte / short / int 类型的值
  • case 语句的值的类型必须和 switch 语句的值相同,且仅支持常量表达式(不能在表达式中包含变量)

条件运算符:boolean-expression ? exp1 : exp2

运算符优先级:

var++, var--
+, - (Unary plus and minus), ++var, --var
(type) Casting
! (Not)
*
, /, % (Multiplication, division, and remainder)
+, - (Binary addition and subtraction)
<, <=, >, >= (Relational operators)
==, != (Equality)
^ (Exclusive OR)
&& (Conditional AND) Short-circuit AND
|| (Conditional OR) Short-circuit OR
=, +=, -=, *=, /=, %= (Assignment operator)

如果具有相同优先级的运算符彼此相邻,它们的结合性决定了计算顺序:除了赋值运算符(它是右结合 (right-associative))之外,所有二元运算符都是左结合 (left-associative)

Mathematical Functions, Characters, and Strings⚓︎

java.lang.Math⚓︎

  • 类常量:PIE
  • 类方法:

    • 三角函数:

      sin(double a)
      cos(double a)
      tan(double a)
      acos(double a)
      asin(double a)
      atan(double a)
      
    • 指数函数:

      exp(double a)
      log(double a)
      log10(double a)
      pow(double a, double b)
      sqrt(double a)
      
    • 舍入函数:

      double ceil(double x)
      double floor(double x)
      double rint(double x)     // 舍入到最近的整数;如果离两个整数一样近,那就舍入到偶数
      int round(float x)        // (int) Math.floor(x + 0.5) 
      long round(double x)      // (long) Math.floor(x + 0.5)
      
    • max(a, b)min(a, b)

    • abs(a)
    • 随机函数:random(),返回 [0.0, 1.0) 范围内的随机 double

Character Data Type⚓︎

  • Java 字符使用 Unicode,占用 2 个字节,以 \u 开头,表示为四个十六进制数字,从 \u0000\uFFFF
  • 递增运算符 ++ 和递减运算符 -- 也可以用于 char 变量,以获取下一个或前一个 Unicode 字符
  • 字符和整数类型间的转换:

    int i = 'a';    // Same as int i = (int)'a';
    char c = 97;    // Same as char c = (char)97;
    
  • 字符类方法:

    isDigit(ch)             // 是否为数字
    isletter(ch)            // 是否为字母
    isletterOrDigit(ch)     // 是否为字母或数字
    isLowerCase(ch)         // 是否为小写字母
    isUpperCase(ch)         // 是否为大写字母
    tolowerCase(ch)         // 转小写字母
    toUpperCase(ch)         // 转大写字母
    

String Type⚓︎

  • String 实际上是 Java 库中的一个预定义类,被称为引用类型(reference type),而不是基本类型 (primitive type)
  • String 对象的简单方法(实例方法(instance methods)

    length()            // 字符串长度
    charAt(index)       // 指定索引下的字符
    concat(s1)          // 和另一个字符串拼接
    toUpperCase()       // 所有字母转大写
    toLowerCase()       // 所有字母转小写
    trim()              // 移除字符串两边的空白字符
    
    • 调用实例方法的语法:referenceVariable.methodName(arguments)
    • 非实例方法被称为静态方法(static methods),可以在不使用对象的情况下调用
  • 比较字符串:

    equals(s1)                  // 两个字符串是否相等
    equalsIgnoreCase(s1)        // 同 equals,但大小写不敏感
    compareTo(s1)               // 字符串是否大于 s1,返回一个整数,
                                // 正数/0/负数分别表示比 s1 大/相等/小
    compareToIgnoreCase(s1)     // 同 compareToIgnoreCase,但大小写不敏感
    startswith(prefix)          // 字符串是否以特定前缀开始
    endsWith(suffix)            // 字符串是否以特定后缀结束
    
    • 注意:使用 == 是在比较两个引用是否指向同一个对象(一般不会这么做,往往是搞错用法了)
  • 获取子字符串:

    substring(beginIndex)               // 从 beginIndex 开始到最后的子字符串
    substring(beginIndex, endIndex)     // 从 beginIndex 开始到 endIndex - 1
                                        // (不包括 **endIndex**)的子字符串
    
  • 在字符串中寻找字符或子字符串

    indexOf(ch)                 // 字符 ch 第一次出现时的索引,若没找到返回 -1
    indexof(ch, fromIndex)      // 从 fromIndex 开始,字符 ch 第一次出现时的索引,若没找到返回 -1
    indexOf(s)                  // 字符串 s 第一次出现时的索引,若没找到返回 -1
    indexOf(s, fromIndex)       // 从 fromIndex 开始,字符串 s 第一次出现时的索引,若没找到返回 -1
    
    // 寻找最后一次出现的索引,和前 4 个方法对应
    lastIndexOf(ch)
    lastIndexOf(ch, fromIndex)
    lastIndexOf(s)
    lastIndexOf(s, fromIndex)
    
  • 字符串与数字的转换:

    // 字符串 —> 数字
    int intValue = Integer.parseInt(intString);
    double doubleValue = Double.parseDouble(doubleString);
    
    // 数字 -> 字符串
    String s1 = String.valueOf(number1);
    String s2 = number2 + "";
    
    • 实际上 valueOf 做的事是直接调用并返回该对象的 toString() 方法(除了 null 对象返回字符串 "null",后者是由 Object 定义的
      • 对于自定义类,我们可以重写 toString() 方法,返回一个包含人类可读的字符串,并能合理描述类的对象的 String 对象
  • + 运算:

    "I’m " + 18     // "I’m 18"
    1 + 2 + "age"   // "3age"
    "age" + 1 + 2   // "age12"
    
  • (block) 语法(和 Python 类似

    String html = """
                    <html>
                    <body>
                        <p>Hello, world</p>
                    </body>
                    </html>
                    """;
    
    • 起止各一行 """ ,单独占一行(开头 """ 后不能直接跟内容)
    • 内容自动去掉公共前缀空白,保留相对缩进
  • String不可变的(immutable),即创建 String 实例后内容无法再更改

    • 因此没有任何函数能改变字符串的内容
    • 但声明为 String 引用的变量可以在任何时候改变以指向其他 String 对象
    为什么要“不可变”
    • 并发 / 线程安全
      • 无状态:对象只读,天然支持多线程共享,无需同步
      • Race-free:写并发代码时不用担心“读到一半被修改”
    • 哈希与索引
      • hashCode 缓存:计算一次后缓存到字段 hash ,后续 HashMap/get 直接复用,复杂度从 O(n) -> O(1)
      • Key 可信:作为HashMap / HashSet key 时,中途内容变化导致哈希漂移的灾难不可能发生(对比 char[]StringBuilder
    • 字符串常量池(StringTable)
      • intern() 复用:字面量自动入池,相同内容全局一份,节省堆内存
      • 地址比较:"foo"=="foo" 直接返回 trueJVM 级别优化
    • 安全性与完整性
      • 类加载器隔离:类名、文件路径、权限字符串一旦传入就无法被篡改,防止“在 check 之后、use 之前”被恶意代码改掉
      • 网络 / 文件句柄:new URL("http://xxx") 的协议、主机名不可变,避免校验后地址被替换
    • 编译器 & JVM 优化
      • 字符串折叠:编译期常量表达式 "a" + "b" 直接变成 "ab" ,减少运行时拼接
      • 栈上优化:逃逸分析后不可变对象可拆成标量替换,消灭堆分配
      • 共享子串:JDK 7 以前 substring 共享底层 char[](offset + count,避免复制;JDK 7 之后虽然改为复制,但仍保留不可变语义,让 JIT 放心做循环不变量外提等优化
    • 除不可变的 String 类型外,还有两个常用的可修改的字符串类型:

      • StringBuffer:线程安全,同步,性能稍慢
      • StringBuilder:非线程安全,不同步,性能更快
      // StringBuilder 示例(单线程)
      StringBuilder sb1 = new StringBuilder("Hello");
      sb1.append(" World");
      
      // StringBuffer 示例(多线程)
      StringBuffer sb2 = new StringBuffer("Hello");
      sb2.append(" World");
      
    • 如果需要不断运算逐渐形成大的字符串,应该使用 StringBuffer

      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < 100; i++)
          sb.append("" + i);
      String s = sb.toString();
      

更多 String API 官方文档

Input: The Scanner Class⚓︎

使用步骤:

  1. 导入包:import java.util.Scanner;
  2. 创建对象:连接到输入流(如 System.in
  3. 读取数据:调用相应的方法获取输入
  4. 关闭资源:使用完毕后关闭流(良好的编程习惯)
例子
import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        // 1. 创建 Scanner 对象,读取标准输入
        Scanner sc = new Scanner(System.in);

        System.out.println("请输入一个整数:");
        // 2. 读取数据
        if (sc.hasNextInt()) {
            int num = sc.nextInt();
            System.out.println("你输入的是:" + num);
        }

        // 3. 关闭资源
        sc.close();
    }
}

常用读取方法:

  • next():读取下一个标记 (token),以空白符(空格、回车等)作为分隔
  • nextLine():读取整行内容,直到遇到回车符
  • nextInt():将下一个标记解析为 int 类型
  • nextDouble():将下一个标记解析为 double 类型
  • hasNext...():判断是否还有下一个指定类型的内容,常用于校验输入

注意事项

  • nextInt()nextLine() 混用问题:
    • 当先调用 nextInt() 再调用 nextLine() 时,nextInt() 只会读取数字,而数字后面的回车换行符会留在缓冲区;接着调用的 nextLine() 会直接读取到这个回车符,导致看起来好像“跳过”了输入
    • 解决方法:在 nextInt() 之后多写一行额外的 nextLine() 来消费掉那个多余的回车符
  • 性能问题:内部使用正则表达式进行匹配,虽然功能强大且方便,但读取速度较慢
    • 如果对性能需求敏感,建议改用 BufferedReader 以获得更好的性能

Formatted Output⚓︎

格式化输出语句:System.out.printf(format, items)。其中 format 是一个包含字符串和格式说明符的字符串。格式说明符(format specifier) 指定 item(可能是一个数值、字符、布尔值或字符串)应该如何显示,且都以百分号开始。

常用说明符:

  • %b:布尔值
  • %c:字符
  • %d:整数
  • %f:浮点数
  • %e:采用标准科学计数法的数字
  • %s:字符串

占位符完整格式为:%[index$][标识]*[最小宽度][.精度]转换符

  • %:占位符的起始字符,若要在占位符内部使用 %,则需要写成 %%
  • [index$]:位置索引从 1 开始计算,用于指定对索引相应的实参进行格式化并替换掉该占位符
  • [标识]:用于增强格式化能力,可同时使用多个 [标识],但某些标识是不能同时使用的
  • [最小宽度]:用于设置格式化后的字符串最小长度,若使用 [最小宽度] 而无设置 [标识],那么当字符串长度小于最小宽度时,则以左边补空格的方式凑够最小宽度
  • [.精度]:对于浮点数类型格式化使用,设置保留小数点后多少位
  • 转换符:用于指定格式化的样式,和限制对应入参的数据类型

字符、字符串的格式化:

  • 占位符格式:%[index$][标识][最小宽度][转换符]
  • 可用标识:
    • -:在最小宽度内左对齐,右边用空格补上
  • 可用转换符:
    • s:字符串类型
    • c:字符类型,实参必须为 charintshort 等可转换为 char 类型的数据类型,否则抛 IllegalFormatConversionException 异常
    • b:布尔类型,只要实参为非 false 的布尔类型,均格式化为字符串 true,否则为字符串 false
    • n:平台独立的换行符(与通过 System.getProperty("line.separator") 是一样的)

整数的格式化:

  • 占位符格式:%[index$][标识]*[最小宽度]转换符
  • 可用标识:

    • -:在最小宽度内左对齐,不可以与 0 标识一起使用
    • 0:若内容长度不足最小宽度,则在左边用 0 来填充
    • #:对 8 进制和 16 进制,8 进制前添加一个 016 进制前添加 0x
    • +:结果总包含一个 +-
    • 空格:正数前加空格,负数前加 -
    • ,:只用于十进制,每 3 位数字间用 , 分隔
    • (:若结果为负数,则用括号括住,且不显示符号
  • 可用转换符:

    • b:布尔类型
    • d:整数类型(十进制)
    • x:整数类型(十六进制)
    • o:整数类型(八进制)
    • n:平台独立的换行符,也可通过 System.getProperty("line.separator") 获取

浮点数的格式化:

  • 占位符格式:%[index$][标识][最小宽度][.精度]转换符
  • 可用标识:同整数
  • 可用转换符:
    • b:布尔类型
    • n:换行符
    • f:浮点数型(十进制,显示 9 位有效数字,且会进行四舍五入
    • a:浮点数型(十六进制)
    • e:指数类型(如 9.38e+5
    • g:浮点数型没,比 %f%a 长度短些,显示 6 位有效数字,且会进行四舍五入

日期时间的格式化:

  • 占位符格式:%[index$]t转换符
  • 日期转换符:

    • c:星期六 十月 27 14:21:20 CST 2007
    • F:2007-10-27
    • D:10/27/07
    • r02:25:51 下午
    • T:14:28:16
    • R:14:28
    • b:月份简称
    • B:月份全称
    • a:星期简称
    • A:星期全称
    • C:年前两位(不足两位补零)
    • y:年后两位(不足两位补零)
    • j:当年的第几天
    • m:月份(不足两位补零)
    • d:日期(不足两位补零)
    • e:日期(不足两位不补零)
  • 时间转换符:

    • H24 小时制的小时(不足两位补零)
    • k24 小时制的小时(不足两位不补零)
    • I12 小时制的小时(不足两位补零)
    • i12 小时制的小时(不足两位不补零)
    • M:分钟(不足两位补零)
    • S:秒(不足两位补零)
    • L:毫秒(不足三位补零)
    • N:毫秒(不足 9 位补零)
    • p:小写字母的上午或下午标记,如中文为“下午”,英文为 pm
    • z:相对于 GMT 的时区偏移量,如 +0800
    • Z:时区缩写,如 CST
    • s:自 1970-1-1 00:00:00 起经过的秒数
    • Q:自 1970-1-1 00:00:00 起经过的毫秒

Loops⚓︎

  • while 循环和do-while 循环基本上和 C/C++ 一致
  • 带标签的 break 语句,实现智能跳出语句块(而非跳入)

    • 标签可用在任何语句中,甚至在 if 或块语句中
    first:for (int j = 0; j < 5; j++) {
        second:for (int i = 0; i < 5; i++) {
            if (i == 0) {
                System.out.println(i);
                break first;
            }
        }
    }
    
  • for 循环除了有一套和 C/C++ 基本一致的版本外,Java 5 引入了更加简洁的 for-in 语法,用于数组和容器:

    int[] arr = {1, 2, 3, 4, 5};
    for (int num : arr)
        System.out.println(num);
    

Methods⚓︎

方法(methods) 是一个被分组在一起以执行某一操作的语句集合。

  • 方法签名(method signature):方法名和参数列表的组合
  • 形式参数(formal parameters)(形参:方法头中定义的变量
  • 实际参数(actual parameters)(实参:调用方法时传递的参数值
  • 返回值类型

Java 方法是按值传参的,传递的是变量或引用的副本,所以在方法内对参数修改不会改变方法外的变量或引用。

方法的重载(overloading) 是指在一个类中可以定义多个名称相同但参数列表不同的方法。注意区分重载和重写(overriding):

特性 方法重载 方法重写
发生范围 同一个类中 父子类之间
方法名 必须相同 必须相同
参数列表 必须不同 必须相同
返回值 可以不同 必须相同(或其子类)
判定时间 编译阶段 运行阶段

模糊调用(ambiguous invocation):有时对于方法的调用可能会有两个或更多可能的匹配,但编译器无法确定最具体的匹配,这是一个编译错误

例子
public class AmbiguousOverloading {
    public static void main(String[] args) {
        System.out.println(max(1, 2));
    }

    public static double max(int num1, double num2) {
        if (num1 > num2)
            return num1;
        else
            return num2;
    }

    public static double max(double num1, int num2) {
        if (num1 > num2)
            return num1;
        else
            return num2;
    }
}

Arrays⚓︎

数组(array) 是一种表示相同类型数据集合的数据结构。

Single-Dimensional Arrays⚓︎

  • 数组变量声明:

    datatype[] arrayRefVar;
    datatype arrayRefVar[];      // 可以这么写,但不推荐
    
  • 创建数组:arrayRefVar = new datatype[arraySize];

  • 一步完成数组的声明和创建:datatype[] arrayRefVar = new datatype[arraySize];
  • 数组创建后长度固定不变,其值为 rrayRefVar.length;并且所有元素会被赋予默认值

    • 数值基础类型:0
    • char 类型:\u0000
    • boolean 类型:false
  • 通过索引访问数组元素:arrayRefVar[index]index 范围为 [0, arrayRefVar.length-1]

  • 数组初始化器(intializer):一步完成声明、创建和初始化的简写语法,注意一定要在一条语句内完成,不能拆成多句

    double[] myList = {1.9, 2.9, 3.4, 3.5};
    
    // 以下语句是错误的!
    double[] myList;
    myList = {1.9, 2.9, 3.4, 3.5};
    
  • 数组分配在

  • [] 运算符被预定义为检查数组边界,而且没有指针运算,就不能通过 a+1 得到下一个元素
  • main 函数中的 String[] args 参数表示命令行参数;注意 args[0] 就是第一个参数,程序名没有存储在 args
  • 复制数组:

    • 如果直接使用赋值语句(比如 list2 = list1;,那么这只是让 list2 同时引用了 list1 的对象,并没有真的创建数组副本(浅拷贝

    • 正确实现(深拷贝

      • 手动实现:新建数组,利用循环逐元素复制

        int[] sourceArray = {2, 3, 1, 5, 10};
        int[] targetArray = new int[sourceArray.length];
        for (int i = 0; i < sourceArrays.length; i++)
            targetArray[i] = sourceArray[i];
        
      • System.arraycopy(sourceArray, src_pos, targetArray, tar_pos, length) 函数

        • 例子:System.arraycopy(sourceArray, 0, targetArray, 0, sourceArray.length);
      • Arrays.copyOf(array, length) 方法

        • 2 个参数是新数组的长度,这个方法通常用来改变数组的大小
          • 如果数组元素是数值型,则多余的元素被赋值为 0;若是布尔型,则赋值为 false
          • 如果长度小于原始数组长度,则只拷贝前面的元素
        • 例子:

          int[] copiedLuckyNumbers = Arrays.copyOf(luckNumbers, 2 * luckyNumbers.length);
          
  • 匿名数组(anonymous array):数组没有对应的显式引用变量

    • 例子printArray(new int[]{3, 1, 2, 6, 4, 2});printArray() 是自定义函数)
  • 虽说 Java 按值传递参数,但对于数组这种引用类型而言,复制到方法中的参数值是指向数组实际内容的引用,因此修改数组参数是会实际影响到数组内容的

MultiDimensional Arrays⚓︎

  • 声明 / 创建二维数组:

    // 声明
    dataType[][] refVar;
    
    // 创建数组并赋值
    refVar = new dataType[10][10];
    // 注意:声明时可以缺省第二维的长度,但必须要指明第一维的长度
    refVar = new dataType[10][];    // OK
    refVar = new dataType[][10];    // Error!
    
    // 二合一
    dataType[][] refVar = new dataType[10][10];
    
    // 另一种语法
    dataType refVar[][] = new dataType[10][10];
    
  • 数组初始化器:使用简写记号完成声明 + 创建 + 初始化

    int[][] array = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
        {10, 11, 12}
    };
    
  • 长度:

  • 二维数组的每一行可以有不同的长度,这样的数组称为锯齿状数组(ragged array)

    int[][] matrix = {
        {1, 2, 3, 4, 5},
        {2, 3, 4, 5},
        {3, 4, 5},
        {4, 5},
        {5}
    };
    
    /**
    matrix.length is 5
    matrix[0].length is 5
    matrix[1].length is 4
    matrix[2].length is 3
    matrix[3].length is 2
    matrix[4].length is 1
    */
    

Wrapper Classes⚓︎

  • 包装类(wrapper class):一个其对象包装或包含基本数据类型的类。当我们创建一个包装类的对象时,它包含一个字段,在这个字段中我们可以存储基本数据类型。换句话说,我们可以将基本值包装进包装类对象中。

    • 包装类包括:

      Byte
      Short
      Integer
      Long
      Float
      Double
      Char
      Boolean
      
  • 创建包装对象

    • 所有包装类型的 public 构造器 已在 JDK 9 被标记为 @Deprecated(since="9", forRemoval=true),并在 JDK 16 正式移除;现在无论 new Integer(123) 还是 new Double(3.14) 都会编译失败
    • 官方理由:
      • 构造函数每次强制生成新对象,浪费内存
      • valueOf 工厂方法利用内部缓存(-128~127 默认缓存在 IntegerCache
      • 字符串驻留、Booleantrue / false单例(类的对象有且仅有一个)保持一致语义
      • 为未来值类型(Valhalla)铺路,彻底消灭包装对象
      • 正确的做法:

        // 1. 工厂方法(推荐)
        Integer a = Integer.valueOf(123);
        
        // 2. 自动装箱(语法糖,底层仍调 valueOf)
        Integer b = 123;
        
        // 3. 字符串解析
        Integer c = Integer.valueOf("123");
        
        // 4. 若需要创建“绝对新对象”——几乎没这需求,真需要就 new 完后别放入任何缓存
        // (JDK 16 以后已无法做到,必须反射或 MethodHandles,但属于黑魔法)
        
  • 自动装箱(auto-box) 自动拆箱(auto-unbox):包装类型和对应的基础类型之间可以直接赋值

    class Geeks {
        public static void main(String[] args) {
            int b; // Primitive data type
            Integer a; // Integer wrapper class
    
            b = 357; // assigning value to primitive
    
            a = b; // auto-boxing or auto wrapping converting primitive int to Integer object
    
            System.out.println("The primitive int b is: " + b);
            System.out.println("The Integer object a is: " + a);
        }
    }
    
  • 预生成对象(pre-created object):落在以下范围内,用的还是同一对象,否则就会创建新的对象

    • 对于整数类型:-128~127
    • 对于字符类型:0~127
    • 对于布尔:truefalse
    • 对于浮点数:没有预生成对象
    Integer i1 = 12; // what if 1200
    Integer i2 = 12; // what if 1200
    System.out.println(i1 == i2);  // == 判断两个指针是否指向同一个对象
    // true,如果是 1200(超范围了)就是 false
    

Classes and Objects⚓︎

  • 定义一个类

    class Main {
        public Main(int i) { _i = i; }
        public int get() { return _i; }
        private int _i;
    }
    
    • 在闭合花括号后没有 ;C++ 是有的)
    • 类的首字母大写,而变量或函数的首字母小写
    • 函数体位于类的花括号内
    • 每个成员要指定访问控制修饰符(比如 public
    • 构造函数中没有初始化列表(好像是 C++ 特有的语法)
  • 编译单元(compliation unit)

    • 每个编译单元必须有一个以 .java 结尾的名称,并且在该编译单元内部可以有一个 public 类,该类必须与文件名相同
    • 每个编译单元中只能有一个 public
    • 当你编译一个 .java 文件时,你会得到一个与文件名完全相同但扩展名为 .class 的输出文件,每个 .java 文件中的每个类都有一个这样的文件
    • 一个可运行的程序是一堆 .class 文件
  • 创建一个对象:唯一的方式是使用 new 运算符

    class Value {
        private int i;
        public Value(int ii) { i = ii;}
        public int get() { return i;}
    }
    public class Main {
        public static void main(String[] args) {
            Value v = new Value(12);
            System.out.println(v.get());
        }
    }
    
  • 赋值:赋值“从一个对象到另一个对象”是指将句柄从一个位置复制到另一个位置

  • 按值传递

    例子
    void f(int n) {
        n = 9;
    }
    int k;
    k = 10;
    f(k);
    
    class Number {
        public int i;
    }
    void f(Number n) {
        n.i = 9;
    }
    Number k = new Number();
    k.i = 10;
    f(k);
    
  • 关系表达式

    • ==, !=能处理任意对象,但是要注意
      • 对于基本数据类型,比较的确实是值
      • 对于引用数据类型,比较的却是对象的内存的地址,即判断是否指向同一个对象
  • switch 表达式

    • Java 12 开始,引入了 switch 表达式,它是对传统 switch 语句的增强
    • 传统的 switch语句(statement),只能执行分支逻辑;而新的 switch 表达式可以返回值,更加简洁、安全
    int day = 3;
    String result = switch (day) {
        case 1 -> "Monday";
        case 2 -> "Tuesday";
        case 3 -> "Wednesday";
        case 4 -> "Thursday";
        case 5 -> "Friday";
        case 67 -> "weekend" // 多个值合并
        default -> "Invalid day";
    } ;
    System.out.println(result); // 输出 "Wednesday"
    
    • 使用 -> 箭头语法,避免了传统 break 的繁琐
    • switch 表达式返回一个值,可以直接赋给变量
    • 支持多个 case 合并(用逗号分隔)
    • 必须覆盖所有可能情况,否则需要 default
    • yield:如果想在 switch 分支里写更复杂的逻辑,需要用 yield 返回值

      int day = 3;
      String result = switch (day) {
          case 1, 2, 3 -> {
              String prefix =
              yield prefix + day; "Early week: ";
              // 用 yield 返回结果
          }
          case 4, 5 -> "Mid week";
          case 6, 7 -> "Weekend";
          default -> throw new IllegalArgumentException("Invalid day: " + day);
      };
      System.out.println(result); // 输出 "Early week: 3"
      
    • Java switch 表达式之所以不需要 break,是因为 switch 表达式只会匹配其中一个 case,一旦匹配上就执行箭头后面的操作,随后立即退出,不会检查后续的 case

  • 成员初始化

    • Java 竭尽全力确保任何变量在使用前都得到正确初始化
    • 由于任何方法都可以初始化或使用该数据,所以强制用户在数据被使用前将其初始化为适当的值可能并不实际。因此,类的每个基本数据类型成员都会被保证获得一个初始值 0
    • 指明初始化

      class Measurement {
          boolean b = true;
          char c = 'x';
          int i = 47;
      };
      
      class Measurement2 {
          Depth o = new Depth();
      };
      
      class CInit {
          int i = f(); // ...
      }
      class CInit {
          int i = f();
          int k = g(i); // ...
      };
      
      class CInitWrong { // this is wrong
          int j = g(i);
          int i = f(); // ...
      };
      
    • 初始化的顺序:在一个类中,初始化的顺序由类中定义变量的顺序决定(和 C++ 一样)

  • this 是一个代理构造函数(delegating ctor)

    • 关键字 this 产生被调用的对象方法的引用
    • this 能显式调用另一个构造函数
    public class Foo {
        Foo(int x) {
            System.out.println("x=" + x);
        }
    
        Foo(int x, int y) {
            this(x); // ✅ 合法,必须是第一条语句
            System.out.println("y=" + y);
        }
    }
    
  • 清理 (cleanup)finalize()

    • 当垃圾收集器准备释放用于存储对象的内存时,它将首先调用其 finalize() 方法
    • 但这个方法和 C++ 的析构函数截然不同
      • 垃圾回收 != 析构
      • 对象可能无法被垃圾回收
      • 垃圾回收仅关乎内存

Static Members⚓︎

  • 静态成员当然还是类的成员
  • Class 对象

    • Class 对象用于创建类中所有的“常规 (regular)”对象。每当创建一个新的类,也会创建一个单独的 class对象(并且相应地会被存储在一个同名 .class 文件中)
    • 运行时,当想要创建某个类的对象时,执行程序的 JVM 首先会检查该类型的 Class 对象是否已加载。如果未加载,JVM 将通过查找具有该名称的 .class 文件来加载它
  • 初始化

    • 静态成员将在被加载到类时初始化
    • 初始化的顺序(举个例子)
      1. 第一次创建类型为 Dog 的对象,或者第一次访问 Dog 类的静态方法或静态字段时,Java 解释器必须定位到 Dog.class
      2. 随着 Dog.class 的加载,其所有的静态初始化器 (initializer) 都会被执行
      3. 当你创建一个新的 Dog 时,Dog 对象的构造过程首先在堆上为 Dog 对象分配足够的存储空间
      4. 该存储空间被清零,自动将该 Dog 对象中的所有原始数据类型设置为它们的默认值
      5. 在字段定义点发生的任何初始化都会被执行
      6. 构造函数将被执行
    • 显式静态初始化:Java 允许在类中特别地“静态构造子句 (static construction clause)”(有时称为静态块 (static block))内分组其他静态初始化
    • 显式初始化:Java 为每个对象初始化非静态变量提供了类似的语法

Packages⚓︎

Java (packages) 是对程序的一种组织。

  • import

    • import 关键字用于引入整个库或该库的一个成员

      import java.util.*;
      import java.util.Vector;
      
    • 包提供了一种管理“命名空间(name space)”的机制

  • 静态导入

    // 一般导入
    import java.lang.Math;
    double r = Math.cos(Math.PI * theta);
    
    // 静态导入
    import static java.lang.Math.PI;
    import static java.lang.Math.*;
    double r = cos(PI * theta);
    
  • 定义一个包

    • 回忆一下:包提供了一种管理“命名空间(name space)”的机制
    • 库也是一堆这样的类文件

      package mypackage;
      public class MyClass {
          // ...
      }
      
    • 如果有人想使用 MyClass,那 ta 就得必须使用 import 关键字来使 mypackage 中的名称可用

      import mypackage.*;
      MyClass m = new MyClass();
      
  • CLASSPATH

    • 将特定包的所有 .class 文件放入单个目录中
    • CLASSPATH 包含一个或多个用作搜索 .class 文件的根目录
    • 或可以使用 -cp 选项在 java 命令中使用
  • C/C++, Python 比较

    编程语言 语法 实现
    C/C++ #include <stdio.h> 文本插入,编译时只看原型,链接时需要编译后的二进制代码
    Java import java.util.Scanner; 装载类,用 RTTI 了解类,编译和运行时均需要编译后的二进制代码,会自动编译
    Python import Pandas 装载运行 Pandas.py 文件,需要源码可见

Access Control⚓︎

  • Java 的访问说明符 (access specifiers)(在类的每个成员(无论是字段还是方法)的每个定义前放置

    • "friendly"

      • 没有任何修饰符,那么该成员能被同一个包内所有的类访问
      • 默认包 (default package):所有未在任何包中声明且位于同一文件夹中的类都在默认包中
    • public:谁都可以访问

    • private:只允许相同类访问
    • protected(有些 "friendly":派生类以及同一个包内的类能够访问
  • 类的访问:

    • 每个类都有一个访问控制修饰符
    • 每个编译单元(文件)只能有一个公共类
    • 公共类的名称必须与文件名完全匹配
    • 虽然不典型,但可以有完全没有公共类的编译单元,在这种情况下可以随意命名文件

评论区

如果大家有什么问题或想法,欢迎在下方留言~