Java 基础和面向对象
- 数据类型
- 流程控制语句
JDK:JRE + 编译器 + 调试器
JRE:虚拟机 + 运行库
JSR:Java 规范
JCP:Java 组织
RI:参考实现
TCK:兼容性测试套件
Java SE JDK 下载地址:https://www.oracle.com/java/technologies/javase-downloads.html
安装之后,要注意环境变量的设置,如果有多个JDK,需要把你要的放在最前面。
java:启动JVM,运行字节码文件
javac:编译 Java 源文件
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
类名,首字母大写,采用大驼峰命名法。
方法,可执行的代码块,首字母小写,采用小驼峰命名法。
public static void main(String[] args)
是Java程序的固定入口方法。
缩进不是必须,是为了好看,一般4个空格。
一个文件,只能有一个 public 的类,源代码文件名,必须是 类名.java 。
- Java程序基本结构
- 变量和数据类型
- 整数运算
- 浮点数运算
- 布尔运算
- 字符和字符串
- 数组类型
面向对象的语言,基本单位:class
类中有若干方法,修饰符 返回类型 方法名 方法参数 方法体
每行语句必须以分号结束
有三种注释,单行的、多行的、文档的
变量两种:基本类型的变量 + 引用类型的变量
变量必须先定义后使用,未指定初始值则有默认值
常量,加了 final 修饰符的变量,常量名全部大写,必须初始化
CPU可直接运算的类型:
- 整数:byte short int long
- 浮点数:float double
- 字符:char(可表示ASCII、Unicode)
- 布尔:boolean(true false)
最小存储单元:byte(信息存储的基本单位)
最小信息单元:bit
┌───┐
byte │ │
└───┘
┌───┬───┐
short │ │ │
└───┴───┘
┌───┬───┬───┬───┐
int │ │ │ │ │
└───┴───┴───┴───┘
┌───┬───┬───┬───┬───┬───┬───┬───┐
long │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
┌───┬───┬───┬───┐
float │ │ │ │ │
└───┴───┴───┴───┘
┌───┬───┬───┬───┬───┬───┬───┬───┐
double │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
┌───┬───┐
char │ │ │
└───┴───┘
float 类型需要加 f 后缀,long 类型需要加 l 后缀。
除了基本类型之外的,都是引用类型。例如 String。
类型名太长时,可用 var 替代,编译器根据 赋值语句 自动推断变量类型。
StringBuilder sb = new StringBuilder();
var sb = new StringBuilder();
{} 定义了语句块,变量作用域从定义处开始,到语句块结束。
定义变量的原则:作用域最小化,尽量定义在尽可能小的作用域,且不重复使用变量名。
遵循四则运算,数值和结果都是整数。
范围限制,存在溢出问题。
+=、-=、*=、/= 简写运算符。
++、– 自增自减,注意前后位置的不同。
«、» 移位运算,相当于乘 除以 2的多少次方,对于负数最高位不动,结果仍是负数,而整数可能变成负数。
«<、»> 带符号的移位运算,符号也会跟着移动。
&、|、~、^ 位运算,与 或 非 异或,两个整数的位运算就是按位对齐,然后对每一位进行运算。
运算符优先级,括号最高,赋值最低,单目高于双目。
类型自动提升,结果为较大类型的,强制类型转换,大范围转成小范围,例如 short s = (short) i;
。
浮点数无法精确表示,例如十进制的0.1换成二进制是个无限循环小数(乘二取整,顺序排列)。
浮点数存在误差,比较两个浮点数是否相等时常常出错,正确的方法是比较两数之差是否小于一个很小的数。
double r = Math.abs(x - y);
if (r < 0.00001) {
} else {
}
整数运算在除数为0时报错,浮点数运算在除数为0时不报错,但返回几个特殊值:NaN、Infinity、-Infinity
强制类型转换时,浮点转整数直接丢弃小数部分,如果要四舍五入则需先加上0.5,转型后超过能表示的最大值则返回最大值。
关系运算,比较运算符(> < >= <= != ==)、与或非(&& || !)
关系运算符的优先级:单目最高,短路或最低,短路与第二低
三元运算符:b ? x : y
。
单引号表示字符,双引号表示字符串。
一个char
保存一个Unicode字符,Java在内存中总是使用Unicode表示字符,所以,一个英文字符和一个中文字符都用一个char
类型表示,它们都占用两个字节。要显示一个字符的Unicode编码,只需将char
类型直接赋值给int
类型即可。
还可以直接用转义字符 \u+ Unicode编码
来表示一个字符
字符串连接:+
多行字符串:利用 + ,或者从Java 13开始,字符串可以用"""..."""
表示多行字符串(Text Blocks)了,多行字符串前面共同的空格会被去掉(总是以最短的行首空格)。
不可变特性:字符串是引用类型且不可变。
引用类型变量可以指向 null,表示变量不指向任何对象。
引用类型。
如何定义? 类型[]
并且必须初始化new 类型[n]
。
数组所有元素初始化为默认值,整型0,浮点0.0,布尔false,引用类型null
,数组一旦创建大小不可变。
数组索引从0开始。
使用**数组变量.length
**可获取数组大小。
可直接在定义数组时指定初始化元素,不必写出数组大小,编译器自动推算,例如int[] ns = {68, 12, 1}
,以及ns = new int[] { 68, 79, 91, 85, 62 }
。
输出可以格式化,%?占位符,详细参数见https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Formatter.html#syntax。
输入稍显复杂:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
System.out.print("Input your name: "); // 打印提示
String name = scanner.nextLine(); // 读取一行输入并获取字符串
System.out.print("Input your age: "); // 打印提示
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
}
}
if (条件1) {
// 条件1满足时执行
}
else if (条件2) {
// 条件1不满足,条件2满足时执行
}
else {
// 条件1,条件2都不满足时执行
}
引用类型判断内容相等要使用equals()
,注意避免NullPointerException
,执行语句s1.equals(s2)
时,如果变量s1
为null
,会报NullPointerException
。
switch
语句可以匹配字符串,比较其内容是否相等,还可以使用枚举类型,换言之,switch
的计算结果必须是整型、字符串或枚举类型。
每个 case 后面都得跟上 break;
。
switch(option) {
case 1: ...; break;
case 2: ...; break;
case 3: ...; break;
default: ...; break;
}
从Java 14开始,switch
语句正式升级为表达式,不再需要break
,并且允许使用yield
返回值。
while 循环:满足条件就执行
while (条件表达式) {
循环语句
}
// 继续执行后续代码
do-while 循环:先执行,再判断
do {
执行循环语句
} while (条件表达式);
for 循环:计数器循环
for (初始条件; 循环检测条件; 循环后更新计数器) {
// 执行语句
}
使用for
循环时,千万不要在循环体内修改计数器!在循环体中修改计数器常常导致莫名其妙的逻辑错误。
使用for
循环时,计数器变量i
要尽量定义在for
循环中,遵循变量定义的作用域最小化原则。
for
循环还可以缺少初始化语句、循环条件和每次循环更新语句。
for-each 循环:常用语遍历数组或可迭代的数据类型,缺点是无法获取索引和执行遍历顺序
for (int n : ns) {
System.out.println(n);
}
循环中有两个特别的语句可使用:break 和 continue。
如何遍历?for循环、for-each循环。
如何排序?常见冒泡排序、插入排序、选择排序、快速排序、堆排序。Java的标准库已经内置了排序功能,我们只需要调用JDK提供的Arrays.sort()
就可以排序,这个方法改变了数组本身,且默认的是升序排序,原理是所谓的双轴快排的算法。
要打印一个多维数组,可以使用多层嵌套的for循环,或者使用Java标准库的Arrays.deepToString()
。
多维数组的每个数组元素长度都不要求相同。
- 面向对象编程
- 异常处理
- 反射
- 注解
- 泛型
- 集合
- IO
- 日期与时间
类如何定义?类有一些字段(此处指public),类的实例需要new出来,可使用 变量.字段
访问实例变量。
方法如何定义?private
方法不允许外部调用,只允许内部调用,this变量始终指向当前实例。
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
可变参数:用类型... 参数名
定义。
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
完全可以把可变参数改写为String[]
类型,但是调用方需要自己先构造String[]
,比较麻烦。
参数绑定机制,不同类型的反应不一样:
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
构造方法:用于初始化实例,构造方法的名称就是类名,没有返回值(也没有void),使用 new 调用该方法。
如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句。
如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法。
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
。
方法名相同,但各自的参数不同(位置、类型、数量),称为方法重载(overload),解决了一个类中,相同功能的方法不同的问题。(方法重载的返回值类型通常都是相同的)
使用extends
关键字来实现继承,子类就获得了父类的所有功能。子类无法访问父类的private
字段或者private
方法。
如果子类与父类有相同的字段,则子类中的字段会代替或隐藏父类的字段,子类方法中访问的是子类中的字段(而不是父类中的字段)。如果子类方法确实想访问父类中被隐藏的同名字段,可以用super关键字来访问它。
子类继承父类的哪些成员(根据访问修饰符来判断):
1:如果父类中的成员使用public修饰,子类无条件继承。 2:如果父类中的成员使用protected修饰,子类也继承,即使父类和子类不在同一个包中。 3:如果父类和子类在同一个包中,此时子类可以继承父类中 缺省修饰符的成员。 4:如果父类中的成员使用private修饰,子类打死也都继承不到。private只能在本类中访问。 5:父类的构造器,子类也不能继承,因为构造器必须和当前的类名相同。
在Java中,没有明确写extends
的类,编译器会自动加上extends Object
。所以,任何类,除了Object
,都会继承自某个类。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问。
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
继承是is关系,组合是has关系。
把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
Person p = new Student();
if (p instanceof Student) {
// 利用instanceof,在向下转型前可以先判断
Student s = (Student) p;
}
使用instanceof variable
这种判断并转型为指定类型变量的语法时,必须打开编译器开关--source 14
和--enable-preview
。
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为重写(Override)。
方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。加上@Override
可以让编译器帮助检查是否进行了正确的覆写。
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。
多态的三个条件:继承、方法重写、父类引用指向子类对象。
如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
。
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承。
对于一个类的实例字段,同样可以用final
修饰。用final
修饰的字段在初始化后不能被修改。
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
abstract class Person {
public abstract void run();
}
如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
抽象类本身被设计成只能用于被继承,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。
尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:
abstract class Person
); - 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
interface
,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有(此处指实例字段,静态字段可以有且只能有!)。因为接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来。
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。
一个类可以实现多个interface
。
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}
default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
在一个class
中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。
还有一种字段,是用static
修饰的字段,称为静态字段:static field
。
不推荐用实例变量.静态字段
去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段
来访问静态对象。
用static
修饰的方法称为静态方法。静态方法只能访问静态字段。
调用静态方法则不需要实例变量,通过类名就可以调用。
interface
是可以有静态字段的,并且静态字段必须为final
类型:
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
interface
的字段只能是public static final
类型,所以可以去掉修饰符。
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
)只是一个简写,真正的完整类名是包名.类名
。包没有父子关系。
没有定义包名的class
,它使用的是默认包,非常容易引起名字冲突。
我们还需要按照包结构把上面的Java文件组织起来。
位于同一个包的类,可以访问包作用域的字段和方法。
还有一种import static
的语法,它可以导入可以导入一个类的静态字段和静态方法。
在代码中,当编译器遇到一个class
名称时:
- 如果是完整类名,就直接根据完整类名查找这个
class
; - 如果是简单类名,按下面的顺序依次查找:
- 查找当前
package
是否存在这个class
; - 查找
import
的包是否包含这个class
; - 查找
java.lang
包是否包含这个class
。
- 查找当前
·定义为public的class、interface可以被其他任何类访问
·定义为private的field、method无法被其他类访问
·protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类
本类中 | 子类中 | 同包类中 | 其他类中 | |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 可以 | 不可以 |
默认 | 可以 | 同包子类可以 | 可以 | 不可以 |
private | 可以 | 不可以 | 不可以 | 不可以 |
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
classpath
的设定方法有两种:
- 在系统环境变量中设置
classpath
环境变量,不推荐; - 在启动JVM时设置
classpath
变量,推荐。
可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件。
jar包实际上就是一个zip格式的压缩文件。
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。
jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF
文件里配置classpath
了。
从Java 9开始,JDK又引入了模块(Module)。
仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件。
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}
其中,module
是关键字,后面的hello.world
是模块的名称,它的命名规范与包一致。花括号的requires xxx;
表示这个模块需要引用的其他模块名。
当我们使用模块声明了依赖关系后,才能使用引入的模块。
class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出了可以访问的包。
module java.xml {
exports java.xml;
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}
只有它声明的导出的包,外部代码才被允许访问。
模块进一步隔离了代码的访问权限。
- 字符串
- StringBuilder
- StringJoiner
- 包装类型
- JavaBean
- 枚举
- 常用工具类
String
内部是通过一个char[]
数组表示的。
字符串比较:必须使用equals()
方法而不能用==
。要忽略大小写比较,使用equalsIgnoreCase()
方法。
还有其他常用方法:
"Hello".contains("ll"); // 因为CharSequence是String的父类。
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
"Hello".substring(2); // "llo",从0开始索引
"Hello".substring(2, 4); // "ll"
" \tHello\r\n ".trim(); // 删除首尾空白:"Hello"
"\u3000Hello\u3000".strip(); // "Hello",和trim()不同的是,类似中文的空格字符\u3000也会被移除
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
String s = "A,,B;C ,D";
s.replaceAll("[\,\;\s]+", ","); // "A,B,C,D"
String s = "A,B,C,D";
String[] ss = s.split(","); // {"A", "B", "C", "D"}
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
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));
// 要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()。
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
// 要把字符串转换为其他类型,就需要根据情况。
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
// String和char[]类型可以互相转换
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
字符编码:Java的String
和char
在内存中总是以Unicode编码表示。
byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象。
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder();
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString());
}
}
StringBuffer
是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!"); // 在不需要指定“开头”和“结尾”的时候,用String.join()更方便
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
我们已经知道,Java的数据类型分两种:
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
。
基本类型 | 对应的引用类型 |
---|---|
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 |
自动装箱与自动拆箱:只发生在编译阶段,目的是为了少写代码。
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
。
所有的包装类型都是不变类!
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较。
包装类型提供了大量实用方法。
// 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,因此,可以非常方便地直接通过包装类型获取各种基本类型
// 向上转型为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中,有很多class
的定义都符合这样的规范:
- 若干
private
实例字段; - 通过
public
方法来读写实例字段。
如果读写方法符合以下这种命名规范:
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
那么这种class
被称为JavaBean
。
boolean
字段比较特殊,它的读方法一般命名为isXyz()
。
我们通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)。
枚举类可让编译器能自动检查某个值在枚举的集合内。
定义枚举类是通过关键字enum
实现的,我们只需依次列出枚举的常量名。
enum
定义的类型就是class
。
默认情况下,对枚举常量调用toString()
会返回和name()
一样的字符串。但是,toString()
可以被覆写,而name()
则不行。
从Java 14开始,引入了新的Record
类。我们定义Record
类时,使用关键字record
。
public record Point(int x, int y) {} // 使用record关键字,可以一行写出一个不变类。
public record Point(int x, int y) {
public Point { // Compact Constructor
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}
在Java中,由CPU原生提供的整型最大范围是64位long
型整数。使用long
型整数可以直接通过CPU指令进行计算,速度非常快。
java.math.BigInteger
就是用来表示任意大小的整数。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数。
和long
型整数运算比,BigInteger
不会有范围限制,但缺点是速度比较慢。
如果BigInteger
表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()
、longValueExact()
等方法,在转换时如果超出范围,将直接抛出ArithmeticException
异常。
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
对BigDecimal
做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断。
必须使用compareTo()
方法来比较,它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于。
数学运算、常量、随机数[0,1)
StrictMath
保证所有平台计算结果都是完全相同的,而Math
会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math
就足够了。
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
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
实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。