Java学习笔记1-JavaSE
其他
通用的版本
jdk8(jdk1.8)
Java程序运行机制
- 编译型
- 解释型

IDEA优化
1.Java基础语法
注释
标识符和关键字
数据类型
- 基本类型: byte,short,int,long,short,float,double,char,boolean
- 引用类型
- 类
- 接口
- 数组
扩展
- 字节:
- 8bit(位)=1Byte(字节)
- 8Byte=1KB
- 1024KB=1M
- 1024M=1G
- 进制: 二进制0b 十进制 八进制0 十六进制0x
- float 避免使用该类型进行比较,使用BigDemical
- 字符编码
- Unicode 2字节 0-65535(2^16)
- char c = '\u0061'
- 转换(byte,short,char->int->long->float->double)
- 高->低:强制转换
- 低->高:自动转换
变量
变量的命名规范
- 局部变量、类成员变量、方法名:首字母小写和驼峰原则
- 常量:大写字母加下划线
- 类名:首字母大写和驼峰原则
包机制
包的概念类似于c#中的命名空间
- 定义包名
package com.bak.www.utils
- 导入包以使用对应类,可使用通配符导入包下的所有class(但不会导入子包的class)
import com.bak.www.utils.*
import static用来导入一个类的所有静态字段和静态方法
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
作用域修饰符
publicprotectedprivatepackage: 不加以上修饰符的类或方法,可以被同一个package下的类访问- 局部变量:方法内部的变量
final:- 修饰
class阻止被继承 - 修饰
method阻止被子类覆写 - 修饰
field、局部变量阻止被重新赋值
- 修饰
一个.java文件只能包含一个public类,但可以包含多个非public类。
编译时查找类的机制
编译器在生成calss的时候,会默认自动
import当前package的其他classimport java.lang.*
所以当编译器遇到一个class名称时,
- 如果是完整类名,直接查找对应的
class - 如果是简单类名,会按照下面的顺序依次查找:
- 查找当前
package是否存在这个class; - 查找
import的包是否包含这个class; - 查找
java.lang包是否包含这个class。
- 查找当前
最佳实践
- 为了避免名字冲突,我们需要确定唯一的包名。通过使用倒置的域名来确保唯一性
- 不要和
java.lang包的类重名,即自己的类不要使用这些名字:- String
- System
- Runtime
- ...
- 不要和JDK常用类重名:
- java.util.List
- java.text.Format
- java.math.BigInteger
- ...
编译和运行
- 编译
src目录下的所有Java文件:
javac -d ./bin src/**/*.java
命令行-d指定输出的class文件存放bin目录,后面的参数src/**/*.java表示src目录下的所有.java文件,包括任意深度的子目录。
windows不支持在命令行使用**,只能列举所有的Java文件进行编译。
- 执行
java -cp ./bin com.itranswarp.sample.Main
JavaDoc
javadoc命令是用于生成api文档的
注释中的参数信息
- @author 作者名
- @version 版本号
- @since 指明需要最早使用的jdk版本
- @param 参数名
- @return 返回值
- @throws 异常抛出情况
命令
javadoc -encoding UTF-8 -charset UTF-8 Doc.java
也可通过IDEA生成,点击工具-生成JavaDoc,设置如下:

classpath 和 jar
classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class。
classpath就是一组目录的集合,它设置的搜索路径与操作系统相关。JVM会按顺序查找,到了对应的class文件,就不再往后继续搜索。如果所有路径下都没有找到,就报错
- 在Windows系统上,用;分隔,带空格的目录用""括起来:
C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
- 在Linux系统上,用:分隔:
/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
classpath的设定方法
- 在系统环境变量中设置classpath环境变量(不推荐);
- 在启动JVM时设置classpath变量(推荐),示例:
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
java -cp abc.xyz.Hello
jar包
实际就是一系列class文件的压缩包,为了便于管理。
- 执行一个jar包的
class
java -cp ./hello.jar abc.xyz.Hello
- 执行jar包
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF文件,MANIFEST.MF是纯文本,可以指定Main-Class和其它信息,如果指定了Main-Class,在执行jar包的时候可以不必指定启动的类名。
java -jar hello.jar
大型项目中,可以直接使用Maven构件jar包,不用手动编写MANIFEST.MF文件。
class版本
查看java、jdk、jvm版本(例如:java 8,Java 11,Java 17
java --version
默认版本
| class文件版本(最高支持) | |
|---|---|
| java 11 | 55 |
| java 17 | 61 |
- 低版本jdk不兼容高版本class(高版本
class文件无法在低版本的jre环境下执行):- 如果
Java 17编译一个Java程序,输出的class文件版本默认就是61,它可以在Java 17、Java 18上运行,但不可能在Java 11上运行,因为Java 11支持的class版本最多到55。 - 但是高版本的Java可以通过指定生成的class来兼容低版本的执行
- 使用
release参数:javac --release 11 Main.java,表示源码兼容Java 11,编译的class输出版本为Java 11兼容,即class版本55 - 使用
sourc(源码版本)和target(class版本)参数:javac --source 9 --target 11 Main.java
- 使用
- 如果
- 高版本jdk兼容低版本class(低版本class文件可以在高版本的环境下执行)
如果运行时的JVM版本是Java 11,则编译时的版本最好一致,也最好使用Java 11,而不是用高版本的JDK编译输出低版本的class。可能会出现方法找不到的情况。
一些语法要求:
- lambda: 8
- var: 10
- switch: 12 --enable-preview
查看class文件版本
javap -verbose Base.class
模块
从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。一个jar包需要依赖另一个jar包才可运行,需要在jar包添加相关说明,使程序可以在编译和运行时能够自动定位到所需的jar包。这种自带依赖关系的class容器就是模块。
- Java的模块在目录
$JAVA_HOME/jmods下 - 模块是以
jmod后缀的文件 - 模块之间的依赖关系已经被写入到模块内的
module-info.class文件了? - 模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本
编写模块
目录结构:
oop-module
├── bin
├── build.sh
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
其中,bin目录存放编译后的class文件,src目录存放源码,按包名的目录结构存放,仅仅在src目录下多了一个module-info.java这个文件,这就是模块的描述文件。在这个模块中,它长这样:
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}
当我们使用模块声明了依赖关系后,才能使用引入的模块。例如,Main.java代码如下:
package com.itranswarp.sample;
//必须引入java.xml模块后才能使用其中的类
import javax.xml.XMLConstants;
public class Main{
public static void Mian(String[] args){
Greeting g = new Greenting();
System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
}
}
编译并运行模块:
- 编译所有
.java文件
java- d bin /src/modeule-info.java src/com/itranswarp/sample/*.java
- 所有class打包成
hello.jar文件,传入--main-class,让这个jar包能自己定位main方法所在的类:
jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .
hello.jar包转换模块
jmod create --class-path hello.jar hello.jmod
- 运行模块 (module-path指定的模块不是jmod后缀,那是否可以跳过第3步?)
java --module-path hello.jar --module hello.world
- 编译模块(真)打包jre
jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/
//运行
jre/bin/java --mpdule hello.world
访问权限
class的访问权限(public、protected、private和默认的包访问权限)在跨模块访问时,被访问的模块需要明确的导出可以访问的包。
例如,hello.world用到了模块java.xml的一个类javax.xml.XMLConstants,之所以能够直接使用这个类,是因为在模块java.xml的module-info.java中声明了若干导出:
module java.xml{
exports java.xml;
exports javax.xml.catalog;
//requires 其他模块的包;
}
流程控制和方法
用户交互Scanner
package com.kuang.scanner;
import java.util.Scanner;
public class Demo{
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
//hasNextLine() hasInt()
String str = scanner.nextLine(); //next()
System.out.println("请输入的内容为:" + str);
scanner.close();
}
}
选择结构
- if
- switch
break、continue
idea 反编译的功能
循环结构
whiledo whilefor//for(int i = 0;i++;i<10) 或者 类似于c#中foreach用法 for(int x: numbers)
方法
可变参数(Jdk1.5之后有的)
一个方法只能指定一个可变参数,且必须是方法的最后一个参数。(类似于C#中params的用法)
public static void printMax(double... numbers){
if(numbers.length == 0){
return;
}
double result = numbers[0];
//...排序
}
递归
数组
定义
int[] nums = new int[3];
int nums2[];
面向对象
面向对象基础
构造方法
方法继承
相同方法名,不同参数,即为方法重载
继承
使用extends关键字(在C#中通过符号“:”)来实现继承:
class Person {
}
class Student extends Person{
}
多态
继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
对于需要覆写的方法,可以加上@Override让编译器帮助检查是否进行了正确的覆写。
如果子类覆写了父类的方法,Java会基于运行时的实际类型进行动态调用,而非变量的声明类型,这个特性在面向对象编程中被称为多态。
class Person {
public void run() {}
}
//子类覆写父类方法
public class Student extends Person {
@Override
public void run(String s) {}
}
//执行某个方法时,如果传入子类,则实际调用的时子类的方法。
public void runTwice(Person p) {
p.run();
p.run();
}
常被覆写的Objects方法
toString()equals()hashCode()
示例:
class Person{
...
@Override
public String toString(){
return "Person:name=" + name;
}
@Override
public boolean equals(object o){
//instance 相当于 C# 中的 is 关键字
if(o instance Person){
Person p = (Person) o;
return this.name.equals(p.name);
}
}
@Override
public int hashCode(){
return this.name.hashCode();
}
}
super:在子类覆写方法中调用父类被覆写的方法
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
class Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
final:不允许再被修改
- 父类不允许子类对某个方法进行覆写
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
- 一个类不希望任何其他类继承自它
final class Person{
}
- 对于一个类的实例字段,用
final修饰的字段在初始化后不能被修改
class Person {
public final String name = "Unamed";
}
抽象类
如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。(定义了抽象方法的类必须为抽象类)。
abstract class Person {
public abstract void run();
}
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:abstract class Person);
- 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
作用和抽象类类似。
接口定义的所有方法默认都是public abstract的,所以可以省略。
使用implements实现接口的方法。
interface Person {
void run(); //等同于 public abstract void run();
String getName();
}
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;
}
}
和抽象类的对比
| abstract class | interface | |
|---|---|---|
| 继承 | 只能 extends一个class | 可以implements多个interface |
| 字段 | 可以定义实例字段 | 不能定义实例字段 |
| 抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
| 非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
继承关系
公共逻辑适合放在abstract class中,具体逻辑放到各个子类,而接口层次代表抽象程度。
参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
default方法
接口中可以通过default关键字修饰方法,子类在不重载的情况下默认调用该方法。
静态字段和静态方法
使用static修饰的字段和方法。通过类名.静态字段和类名.静态方法访问
接口的静态字段
接口的静态字段只能是public static final,所以可以将修饰符去掉。
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
内部类 Inner Class
被定义在另一个类内部的类,就是Inner Class:
class Outer {
class Inner {
// 定义了一个Inner Class
}
}
//创建
Outer outer = new Outer("Nested"); // 实例化一个Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
Outer类被编译为Outer.class,而Inner类被编译为Outer$Inner.class。
匿名类
在方法内部,可以定义匿名类(Anonymous Class)
Runnable r = new Runnable() { //可以是接口或者类
//
};
- 定义一个实现了接口的匿名类:
示例:
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();
}
}
Outer类被编译为Outer.class,而匿名类被编译为Outer$1.class。
- 定义一个继承自普通类的匿名类:
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>(); //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"));
}
}s
静态内部类(Static Nested Class)
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);
}
}
}
Java核心类
字符串
String
Java编译器对String有做了特殊处理,实际字符串在String内部通过一个char[]数组标识,即
String s1 = "Hello!";
//等价于
String s1 = new String(new char[]{'H','e','l','l','o','!'});
所以实际上字符串是不可变的。这种不可变是通过内部的private final char[],以及没有可以修改cahr[]的方法来实现的。
需要特别注意的是,对于字符串比较,必须使用equals()而不是==。Java编译器在编译期,会自动把所有字符串放入常量池,所以:
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true 在同一常量池中,所以引用相同
System.out.println(s1.equals(s2)); //true
String s3 = "HELLO".toLowerCase();
System.out.println(s1 == s3); //false
System.out.println(s1.equals(s3)); //true
}
}
字符串的操作:
//忽略大小写进行比较
"hell".equalsIgnoreCase("HELL"); //true
//是否包含字串
"HEllo".contains("ll"); //true
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
//索引号从0开始
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
//除字符串首尾空白字符。空白字符包括空格,\t,\r,\n
" \tHello\r\n ".trim(); // "Hello"
和trim()类似,但中文的空格字符\u3000也会被移除
"\u3000Hello\u3000".strip(); // "Hello
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
//是否为空、为空白字符串:
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
//替换
"Hello".replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
"hello".replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
//正则表达式替换
"A,,B;C ,D".replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
//分割字符串
String[] ss = "A,B,C,D".split("\\,"); // {"A", "B", "C", "D"}
//拼接字符串
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
格式化字符串
formatted()方法和 format() 静态方法,%s显示字符串,%d显示整数,%x显示十六进制整数,%f显示浮点数。
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));
类型转换
//转换为字符串
String.valueOf(123); //"123"
String.valueOf(45.67); //"45.67"
String.valueOf(true); // "true"
String.vlueOf(new Object()); //类似java.lang.Object@036
//字符串转换为其他类型
int n1 = Integer.parseInt("123"); //123
int n2 = IntegerInt("ff", 16); //按十六进制转换, 255
//字符串转换为`boolean`类型
boolean b1 = Boolean.parseBoolean("true"); //true
boolean b2 = Boolean.parseBoolean("FALSE"); //false
//将字符串对应的系统变量转换为 Integer
Integer.getInteger("java.verison"); //版本号,11
//转换为char[]
char[] cs = "Hello".toCharArray();
String s = new String(cs); //修改cs,不会改变s,因为复制了
字符编码
- ASCII 英文编码
- Unicode 全面的编码
- UTF-8 可变长编码(容错强,传输过程中某些字符出错,不会影响后续字符,因为依靠高字节位来确定一个字符究竟是几个字节)
在Java中,char类型实际上就是两个字节的Unicode编码,使用其他编码需要手动转换
//String 转换为 byte[] (优先考虑 UTF-8 编码)
byte[] b1 = "Hello".getBytes(); //系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8); //按UTF-8编码转换
byte[] b3 = "Hello".getBytes("GBK"); //按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); //UTF-8
//bytes[] 转换为 String
byte[] b = ...
String s1 = new String(b,"GBK");
String s2= new String(b, StandardCharsets.UTF_8); //按UTF-8转换
StringBuilder
Java编译器允许通过+拼接字符串,但通过+每次都会创建新的字符串对象,不但会影响内存,还会影响GC效率。所以为了能够高效的拼接字符串,Java标准库提供了StringBuilder,它是可变对象,可以预分配缓冲区,往StringBuilder中新增字符时,不会创建新的临时对象
StringBuilder sb = new StringBuilder(1024); //设置缓存区大小
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
普通的字符串+操作,不需要改写为StringBuilder,因为在Java编译时就自动把多个连续的+操作编码为StringConcatFactory的操作。在运行期,StringConcatFactory会自动把字符串连接操作优化为数组复制或者StringBuilder操作。
StringBuffer是Java早期的一个StringBuilder的线程安全版本,它通过同步来保证多个线程操作字符串安全。现在完全没有必要使用StringBuffer。
StringJoiner
数组拼接转字符
String[] names = {"Bob", "Alics", "Grace"};
//var sj = new StringJoiner(",");
var sj = new StringJoiner(",", "Hello ", "!");
for(String name: names){
sj.add(name);
}
System.out.println(sj.toString()); //Hello Bob,Alics,Grace!
静态方法String.join(),内部也是使用 StringJoiner:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类型
Java的数据类型:
- 基本类型:byte,short,int,long,boolean,float,double,char
- 引用类型:所有class和interface类型
基本类型可以赋值为null,但基本类型不能赋值为null,想要把基本类型(例如int)视为对象,可以定义一个Integer,它值包含一个实例字段int,这样,Integer类可以视为int的包装类(Wrapper Class):
public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
基本类型对应的包装类型:
| 基本类型 | 对应的引用类型 |
|---|---|
| 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 |
推荐静态方法去定义:
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());
}
}
自动装箱、自动拆箱(编译器完成,JDK>=1.5)
会影响执行效率,且拆箱时可能发生NullPointerException
Integer n = 100; //自动装箱,编译器自动使用Integer.valueOf(int)
int x = n; // 自动拆箱,编译器自动使用Integer.intValue()
不变类
所有包装类型都是不变类,所以对Integer对象进行比较时,应该使用equals() 而不是 ==进行比较。
进制转换
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
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进制
通过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();
无符号整型
通过包装类型的静态方法进行无符号整型和有符号整型的转换
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
JavaBean
将符合这种读写规范的类,称为JavaBean。(getXyz()、setXyz(type value)、isXyz())
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 enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}
默认值和枚举的顺序有关,可以通过如下方式进行自定义值:
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;
}
}
记录类:Recod(>=java14)
record Point(int x,int y){}
相当于:
final class Point extends Record{
private final int x;
private final int y;
public Poitn(int x,int y){
this.x = x;
this.y = y;
}
public int x(){
return this.x;
}
public int y(){
return this.y;
}
public String toString(){
return String.format("Pint[x=%s, y=%s]",x ,y);
}
public boolean equals(Object o){
...
}
public int hashCode(){
...
}
}
构造方法
可以为record类的构造方法上添加检查逻辑:
public record Point(int x, int y){
public Point{
if(x < 0 || y < 0){
throw new IllegalArgumentException();
}
}
}
//相当于:
public final class Point extends Record{
public Point(int x, int y){
if(x < 0 || y < 0){
throw new IllegaArguemtException();
}
}
//编译器生成的赋值代码:
this.x = x;
this.y = y;
}
还可以添加静态函数:
public record Point(int x, int y) {
public static Point of() {
return new Point(0, 0);
}
public static Point of(int x, int y) {
return new Point(x, y);
}
}
BigInteger
对BigInteger做运算的时候,只能使用实例方法,例如,加法运算:
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
//其他例如pow,multiply
转换为基本类型:
- 转换为byte:byteValue()
- 转换为short:shortValue()
- 转换为int:intValue()
- 转换为long:longValue()
- 转换为float:floatValue()
- 转换为double:doubleValue()
上述方法返回的结果如果超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。若想要获取精确的结果,应该使用intValueExact()、longValueExact()等方法,这类方法在转换时如果超出范围,将直接抛出ArithmeticException异常。
BigDecimal
和BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489
//获取小数位数
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
BigDecimal d4 = d2.stripTrailingZeros();
System.out.println(d2.scale()); // 4
System.out.println(d4.scale()); // 2,因为去掉了00
BigDecimal d5 = d3.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d5.scale()); // -2
可以对一个BigDecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
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 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 n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) { //dr[0] 是商,dr[1] 是余数
// n是m的整数倍
}
比较BigDecimal时,使用equals()方法要求值和scale()相等,推荐使用**compareTo()**进行比较,它根据两个值的大小分别返回负数(小于)、整数(大于)、0(等于)。
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变为3
System.out.println(d1.compareTo(d2)); // 0
常用工具类
Math
(还有StrictMath,慢,但是所有平台的结果都一致)
//求绝对值:
Math.abs(-100); // 100
Math.abs(-7.8); // 7.8
//取最大或最小值:
Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2
//计算xy次方:
Math.pow(2, 10); // 2的10次方=1024
//计算√x:
Math.sqrt(2); // 1.414...
//计算ex次方:
Math.exp(2); // 7.389...
//计算以e为底的对数:
Math.log(4); // 1.386...
//计算以10为底的对数:
Math.log10(100); // 2
//三角函数:
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还提供了几个数学常量:
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5
生成随机数:
//默认随机数范围 0<= x < 1
Math.random(); // 0.34241...
//生成 [MIN,MAX) 随机数
double x = Math.random(); // [0,1)
double min = 10, max = 50;
double y = x * (max - min) + min; //[10,50)
long n = (long)y; //[10,50) 整数
HexFormat
byte[] 和十六进制字符串转换
byte[] data = "Hello".getBytes();
//bytes[] => 十六进制字符串
HexFormat hf = HexFormat.of();
String hexData = hf.formatHex(data); //4865c6c6f
//定制转换格式
//分隔符为空格,添加前缀0x,转大写
HexFormat hf = HexFormat.ofDelimiter(" ").whthPrefix("0x").whthUpperCase();
gf.formatHex(data); //0x48 0x65 0x6C 0x6C 0x6F
//十六进制字符串转byte[]
byte[] bs = HexFormat.of().parseHex("4865c6c6f");
Random
用来生成伪随机数。伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的
//默认使用系统的当前时间戳作为种子
Random r = new Random();
r.nextInt(); // 29902222,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 242342342343,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
默认创建Random实例时,如果不给定种子,会默认使用系统的当前时间戳作为种子,因此上面的代码执行结果每次都不一样。
给定种子创建时间戳:
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
// 51, 80, 41, 28, 55...
System.out.println(r.nextInt(100));
}
Math.random()实际上内部调用了Random类。
SourceRandom
SourceRandom是真随机数。无法指定种子,使用RNG(random number generator)算法。
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
JDK的SecureRandom实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
import java.util.Arrays;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
//...
SourceRandom 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));
//...
异常
Java的异常处理机制
try{
}catch(FileNotFoundException e){
//file not found:
}catch(SecuirtyException e){
//no read permissio:
}catch(IOException e){
//io error:
}catch(Exception e){
//other error:
}
Java中异常的继承关系是:
┌───────────┐
│ Object │
└───────────┘
▲
│
┌───────────┐
│ Throwable │
└───────────┘
▲
┌─────────┴─────────┐
│ │
┌───────────┐ ┌───────────┐
│ Error │ │ Exception │
└───────────┘ └───────────┘
▲ ▲
┌───────┘ ┌────┴──────────┐
│ │ │
┌─────────────────┐ ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘ └─────────────────┘└───────────┘
▲
┌───────────┴─────────────┐
│ │
┌─────────────────────┐ ┌─────────────────────────┐
│NullPointerException │ │IllegalArgumentException │...
└─────────────────────┘ └─────────────────────────┘
从继承关系可以看出:Throwable是异常体系的根,有两个体系:Error和Exception,
Error处于严重的错误,程序对此一般无能为力,例如:OutOfMemoryErro: 内存耗尽NoClassDefFoundError: 无法加载某个ClassStackOverflowError: 栈溢出
Exception则是运行时的错误,它可以被捕获处理:- 应用程序逻辑处理的一部分,需要被捕获并处理,例如:
- NumberFormatException: 数值类型的格式错误
- FileNotFoundException: 未找到文件
- SocketException:读取网络失败
- UnsupportedEncodingException
- 程序逻辑编写不对造成的bug,需进行修复
- NullPointerException:
- IndexOutOfBoundsExcepton: 数组索引越界
- 应用程序逻辑处理的一部分,需要被捕获并处理,例如:
Exception 又分为两大类:
RuntimeException以及子类- 非
RuntimeException(包括IOException、ReflectiveOperationException等等)
Java规定:
- 必须捕获的异常,不包括
RuntimeException的Exception及其子类的异常,被称为Checked Exception - 不需要捕获的异常,包括
Error及其子类,RuntimeException及其子类 (编译器RuntimeException及其子类不做强制捕获要求,但是否需要捕获还是需要看具体的问题)
运行以下代码时
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) {
return s.getBytes("GBK");
}
}
编译器会报错:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,这是因为对于UnsupportedEncodingException的错误未进行捕获。
可在方法中捕获它声明的可能抛出的UnsuppotedEncodingException。但还需再调用方法的位置进行捕获,否则编译器依然会抛出异常。
static byte[] toGBK(String s) throws UnsupportedEncodingException {
return s.getBytes("GBK");
}
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息:
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
原始异常添加到新的异常中:
Exception origin = null;
try {
System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
}
throw e;
}
自定义异常
Exception
│
├─ RuntimeException
│ │
│ ├─ NullPointerException
│ │
│ ├─ IndexOutOfBoundsException
│ │
│ ├─ SecurityException
│ │
│ └─ IllegalArgumentException
│ │
│ └─ NumberFormatException
│
├─ IOException
│ │
│ ├─ UnsupportedCharsetException
│ │
│ ├─ FileNotFoundException
│ │
│ └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
自定义的根异常BaseException建议从RuntimeException派生,其他业务类型的异常可以从BaseException派生:
public class BaseException extends RuntimeException{
}
自定义的BaseException应该提供多个构造方法(从继承的RuntimeException抄过来):
public class BaseException extends RuntimeExcetpion{
public BaseException(){
super();
}
public BaseException(String message, Throwable cause){
super(message,cause);
}
public BaseException(String messge){
super(message);
}
public BaseException(Throwable cause){
super(cause);
}
}
NullPointerException
从Java 14开始,对于NullPointerException错误,可以让JVM给出详细信息告诉我们null对象到底是谁,只要再JVM中添加参数-XX:+ShowCodeDetailsInExceptionMessages来启用它:
java -XX:+ShowCodeDetailsInExceptionMessages Main.java
使用断言
语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError:
void sort(int[] arr){
assert x >= 0 : "x must >= 0";
}
对于可恢复的程序错误,不应该使用断言,例如:
void sort(int[] arr) {
assert arr != null; //不推荐
if (arr == null) {
throw new IllegalArgumentException("array cannot be null");
}
}
JVM默认关闭断言指令,即遇到assert语句就忽略不执行,要启用,需要给Java虚拟机传递-enableassertions(简写-ea)参数。
java -ea Main.java
日志
Java标准库内置的日志包java.util.logging:
import java.util.logging.Level;
import java.util.logging.Logger;
public class Hello {
public static void main(String[] args){
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
}
}
日志级别,从严重到普通:
- SERVER
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
日志可通过调整级别,屏蔽与调试相关的日志输出。使用Java标准库内置的Logging再JVM启动时读取配置文件并进行初始化,后续就无法修改配置;且配置不太方便,许再启动时传递参数-Djava.util.logging.config.file=<config-file-name>
Commons Logging
第三方日志库Commons Loggins,默认自动搜索并使用Log4j(日志系统),如果没有找到Log4j,再使用JDK Logging。
Log log = LogFactory.getLog(Main.class);
log.info("start...");
log.warn("end.");
IDEA导入第三方包
- 创建lib文件夹(lib文件夹位于项目根目录,与.idea目录同级),将第三方jar包(commons-logging-1.3.3.jar)复制到lib文件夹下
- 对lib文件建立依赖:选择【文件】-【项目结构】,在此页面导入lib文件夹下的第三方jar包,如下图所示

IDEA
常用类
集合框架
Maven 基础
IO
多线程
网络编程
注册和反射
JUC编程
JVM探究
【扩展】23种设计模式
[扩展]XML
【扩展】数据结构和算法
【扩展】正则表达式
参考
[狂神说]
笔记

浙公网安备 33010602011771号