Basic Java⚓︎
约 3244 个字 180 行代码 预计阅读时间 18 分钟
Data Types and Wrapper Classes⚓︎
- 
Java 的数值基本类型 类型 大小 最小值 最大值 byte1B -128 127 short2B -32768 32767 int4B –2,147,483,648 2,147,483,647 long8B –9,223,372,036,854,775,808 9,223,372,036,854,775,807 float4B 大约 –3.4E+38,有 7 个有效位 大约 3.4E+38,有 7 个有效位 double8B 大约 –1.7E+308,有 15 个有效位 约 1.7E+308,有 15 个有效位 注 Java 没有 unsigned,主要是因为 Java 足够安全健壮(内存安全、类型安全) ,无需unsigned的约束。反观 C/C++ 之所以有 unsigned,是因为表达裸二进制计算(机器中的计算)的需要。
- 
特殊的数字格式 
- 
非数值的基本类型 - boolean:- true,- false
- char:16 位,采用 Unicode-16 编码
 
- 
包装类(wrapper class):一个其对象包装或包含基本数据类型的类。当我们创建一个包装类的对象时,它包含一个字段,在这个字段中我们可以存储基本数据类型。换句话说,我们可以将基本值包装进包装类对象中。 - 
包装类包括: 
 
- 
- 
创建包装对象 - 所有包装类型的 public构造器 已在 JDK 9 被标记为@Deprecated(since="9", forRemoval=true),并在 JDK 16 正式移除;现在无论new Integer(123)还是new Double(3.14)都会编译失败
- 官方理由:- 构造函数每次强制生成新对象,浪费内存
- valueOf工厂方法利用内部缓存(-128~127 默认缓存在 IntegerCache)
- 字符串驻留、Boolean的true/false单例(类的对象有且仅有一个)保持一致语义
- 为未来值类型(Valhalla)铺路,彻底消灭包装对象
- 
正确的做法: 
 
 
- 所有包装类型的 
- 
自动装箱(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
- 对于布尔:true和false
- 对于浮点数:没有预生成对象
 
Strings⚓︎
- 创建 String对象
- String的- +运算:
- String块 (block)(和 Python 的类似- ) :
- 起止各一行 """,单独占一行(开头"""后不能直接跟内容)
- 
内容自动去掉公共前缀空白,保留相对缩进 
- 
String类型和int类型的赋值有所不同:
- 
String的相等性// tests identity(比较两个引用是否指向同一个对象) if (input == "bye") { // ... } // tests equality(这个才是比较内容的相等性) if (input.equals("bye")) { // ... }- String应始终用- .equals()方法做比较
 
更多 String 的 API 见官方文档
- 
Java 中的 String是不可变的(immutable)- 不可变的:创建 String实例后内容无法再更改- String类没有任何函数改变字符串的内容
 
- 然而,声明为 String引用的变量可以在任何时候改变以指向其他String对象- 但仍然无法直接修改 String的内容
 
- 但仍然无法直接修改 
- C++ 是可变的,Python 是不可变的- 因此整个 Java 生态可以放心地把 String当成基础设施:随手缓存、随处共享、当 key、当锁、当常量,而无需拷贝或同步
 
- 因此整个 Java 生态可以放心地把 
 为什么要“不可变”- 并发 / 线程安全- 无状态:对象只读,天然支持多线程共享,无需同步
- Race-free:写并发代码时不用担心“读到一半被修改”
 
- 哈希与索引- hashCode 缓存:计算一次后缓存到字段 hash,后续HashMap/get直接复用,复杂度从 O(n) -> O(1)
- Key 可信:作为HashMap/HashSet的 key 时,中途内容变化导致哈希漂移的灾难不可能发生(对比char[]或StringBuilder)
 
- hashCode 缓存:计算一次后缓存到字段 
- 字符串常量池(StringTable)- intern()复用:字面量自动入池,相同内容全局一份,节省堆内存
- 地址比较:"foo"=="foo"直接返回true,JVM 级别优化
 
- 安全性与完整性- 类加载器隔离:类名、文件路径、权限字符串一旦传入就无法被篡改,防止“在 check 之后、use 之前”被恶意代码改掉
- 网络 / 文件句柄:new URL("http://xxx")的协议、主机名不可变,避免校验后地址被替换
 
- 编译器 & JVM 优化- 字符串折叠:编译期常量表达式 "a" + "b"直接变成"ab",减少运行时拼接
- 栈上优化:逃逸分析后不可变对象可拆成标量替换,消灭堆分配
- 共享子串:JDK 7 以前 substring 共享底层 char[](offset + count) ,避免复制;JDK 7 之后虽然改为复制,但仍保留不可变语义,让 JIT 放心做循环不变量外提等优化
 
- 字符串折叠:编译期常量表达式 
 - 
除不可变的 String类型外,还有两个常用的可修改的字符串类型:- StringBuffer:线程安全,同步,性能稍慢
- StringBuilder:非线程安全,不同步,性能更快
 
 思考- 
如果需要不断运算逐渐形成大的字符串,应该使用 StringBuffer
 
- 不可变的:创建 
- 
转换至 String- 当 Java 在拼接时将数据转换至字符串形式时,它会调用一个由 String定义的String转换方法valueOf()的其中一个重载版本
- valueOf()对所有简单类型和- Object类型都有重载版本
- 对于简答类型,valueOf()返回一个包含人类可读的,和原数据等价的字符串
- 每个类都可以实现 toString()方法,因为它是由Object定义的
- 对于我们自己创建的类,我们可以重写 (override) toString()方法,并提供我们自己编写的简单形式
- 要实现 toString(),只需返回一个包含人类可读的字符串,并能合理描述类的对象的String对象即可
 
- 当 Java 在拼接时将数据转换至字符串形式时,它会调用一个由 
- 
检索来自 String的数据- “类型包装(type wrapper)”类(Integer,Long,Float,Double)提供了一个valueOf()方法,它能将String转换为那个类型的对象
 
- “类型包装(type wrapper)”类(
Classes and Objects⚓︎
- 
定义一个类 - 在闭合花括号后没有 ;(C++ 是有的)
- 类的首字母大写,而变量或函数的首字母小写
- 函数体位于类的花括号内
- 每个成员要指定访问控制修饰符(比如 public)
- 构造函数中没有初始化列表(好像是 C++ 特有的语法)
 
- 在闭合花括号后没有 
- 
编译单元(compliation unit) - 每个编译单元必须有一个以 .java 结尾的名称,并且在该编译单元内部可以有一个 public类,该类必须与文件名相同
- 每个编译单元中只能有一个 public类
- 当你编译一个 .java 文件时,你会得到一个与文件名完全相同但扩展名为 .class 的输出文件,每个 .java 文件中的每个类都有一个这样的文件
- 一个可运行的程序是一堆 .class 文件
 
- 每个编译单元必须有一个以 .java 结尾的名称,并且在该编译单元内部可以有一个 
- 
创建一个对象:唯一的方式是使用 new运算符
- 
赋值:赋值“从一个对象到另一个对象”是指将句柄从一个位置复制到另一个位置 
- 
按值传递 
- 
关系表达式 - ==,- !=能处理任意对象,但是要注意- 对于基本数据类型,比较的确实是值
- 对于引用数据类型,比较的却是对象的内存的地址,即判断是否指向同一个对象
 
 
- 
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 6,7 -> "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 12 开始,引入了 
- 
成员初始化 - Java 竭尽全力确保任何变量在使用前都得到正确初始化
- 由于任何方法都可以初始化或使用该数据,所以强制用户在数据被使用前将其初始化为适当的值可能并不实际。因此,类的每个基本数据类型成员都会被保证获得一个初始值 0
- 
指明初始化 
- 
初始化的顺序:在一个类中,初始化的顺序由类中定义变量的顺序决定(和 C++ 一样) 
 
- 
this是一个代理构造函数(delegating ctor)- 关键字 this产生被调用的对象方法的引用
- this能显式调用另一个构造函数
 
- 关键字 
- 
清理 (cleanup): finalize()- 当垃圾收集器准备释放用于存储对象的内存时,它将首先调用其 finalize()方法
- 但这个方法和 C++ 的析构函数截然不同- 垃圾回收 != 析构
- 对象可能无法被垃圾回收
- 垃圾回收仅关乎内存
 
 
- 当垃圾收集器准备释放用于存储对象的内存时,它将首先调用其 
Static Members⚓︎
- 静态成员当然还是类的成员
- 
Class对象- Class对象用于创建类中所有的“常规 (regular)”对象。每当创建一个新的类,也会创建一个单独的- class对象(并且相应地会被存储在一个同名 .class 文件中)
- 运行时,当想要创建某个类的对象时,执行程序的 JVM 首先会检查该类型的 Class对象是否已加载。如果未加载,JVM 将通过查找具有该名称的 .class 文件来加载它
 
- 
初始化 - 静态成员将在被加载到类时初始化
- 初始化的顺序(举个例子)- 第一次创建类型为 Dog的对象,或者第一次访问Dog类的静态方法或静态字段时,Java 解释器必须定位到 Dog.class
- 随着 Dog.class 的加载,其所有的静态初始化器 (initializer) 都会被执行
- 当你创建一个新的 Dog时,Dog对象的构造过程首先在堆上为Dog对象分配足够的存储空间
- 该存储空间被清零,自动将该 Dog对象中的所有原始数据类型设置为它们的默认值
- 在字段定义点发生的任何初始化都会被执行
- 构造函数将被执行
 
- 第一次创建类型为 
- 显式静态初始化:Java 允许在类中特别地“静态构造子句 (static construction clause)”(有时称为静态块 (static block))内分组其他静态初始化
- 显式初始化:Java 为每个对象初始化非静态变量提供了类似的语法
 
Packages⚓︎
Java 的包(packages) 是对程序的一种组织。
- 
import- 
import关键字用于引入整个库或该库的一个成员
- 
包提供了一种管理“命名空间(name space)”的机制 
 
- 
- 
静态导入 
- 
定义一个包 - 回忆一下:包提供了一种管理“命名空间(name space)”的机制
- 
库也是一堆这样的类文件 
- 
如果有人想使用 MyClass,那 ta 就得必须使用import关键字来使mypackage中的名称可用
 
- 
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"- ) :派生类以及同一个包内的类能够访问
 
- 
- 
类的访问: - 每个类都有一个访问控制修饰符
- 每个编译单元(文件)只能有一个公共类
- 公共类的名称必须与文件名完全匹配
- 虽然不典型,但可以有完全没有公共类的编译单元,在这种情况下可以随意命名文件
 
- 
用 final修饰后的东西无法再改变,可修饰的东西有:- 字段:值无法改变
- 方法:派生类无法重写该方法
- 类:类无法被继承
 
评论区
