管理 Java 多个版本
sudo update-alternatives --config java
sudo update-alternatives --config javac
(一)在Linux系统里
什么是环境变量?
系统级或用户级的变量. 类型与程序/脚本中的变量, 只不过作用域是整个系统或当前用户.
如 /etc/profile.d/
对所有用户都有效.
~/.bashrc
: 只对当前用户有效
环境变量PATH的作用?
当输入命令 grep
等不完整路径的命令时, 系统处理会在当前路径下搜索该程序, 还会到 PATH 中的路径进行搜索. 如: /usr/bin
怎么修改PATH?怎么持久化这个修改(避免被重置)?
临时修改: export PATH=$PATH:your_path
持久化修改:
-
/etc/environment
中修改; 影响所有用户. -
/etc/profile.d/
中创建相应的 bash 脚本, 影响所有用户. -
~/.bashrc
中修改, 只影响当前用户. - 重启系统或
source /etc/environment, source ~/.bashrc
.会立即生效
(二)关于Java
Java之父是谁?
James Gosling
什么是字节码?
字节码: bytecode
javac 会将 .java 文件编译成字节码文件 .class, 可由 jvm 执行.
同一个字节码文件, 可以由不同系统上的 JVM 执行.
其他语言(如 Scala, Kotlin) 也会将源码编译成 bytecode.
什么是JVM?
JVM: Java Virtual Machine
将字节码程序编译成机器码(machine code), 并执行.
包含 JIT compiler(also called HotSpot)
什么是JRE?请说明JRE和JVM的关系。
JVM: 把 bytecode 编译成机器码
JRE: JVM + 核心类库
核心类库需要举例
- 类库: 多个Class文件打包, 就形成了类库.
- dt.jar: Design Time, GUI 相关类库
- tools.jar: javac, java 就是直接调用了 tools.jar
- rt.jar: Run Time,
什么是JDK?请说明JDK和JRE的关系。
JDK: JRE + 开发工具(编译器 javac, jdb, jar)
什么是JDK发行版?请举二例。
JDK 的不同实现. 源码相同, 构建方式不同.
- Oracle JDK
- Microsoft Build of OpenJDK
- Liberica JDK
备注: OpenJDK 不属于发行版, 类似于 Linux 与 Ubuntu.
[图片上传失败...(image-19a79c-1687163467119)]
// (三)先只使用JDK、命令行,不用Maven、IDE等工具……
不使用包管理器,手动安装JDK 11
- 下载 jdk 文件 下载链接
sudo dpkg -i jdk-11.0.16_linux-x64_bin.deb
使用 apt 安装: sudo apt install openjdk-11-jdk
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/
确定 JAVA_HOME 的方法: ll /usr/bin/ | grep java
: 便可查看 java 的真实安装路径
# 创建存放的文件夹
sudo mkdir /usr/java && cd /usr/java
# 下载 jdk 文件
curl https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz -o jdk-17_linux-x64_bin.tar.gz
tar -xvf jdk-17_linux-x64_bin.tar.gz
在 /etc/profile 文件中添加以下内容
# java17
export JAVA_HOME=/usr/java/jdk-17.0.5
export PATH=$JAVA_HOME/bin:$PATH
什么是 main 方法?
每个类的入口方法, 每个类中最多只能定义一个 main 方法. 执行Java程序时, 会首先寻找 main 方法. 如果没有找到该方法, 会抛出异常 Error: Main method not found in class
.
如果定义多个 main 方法, 会抛出以下报错:
$ javac p/A.java
p/A.java:13: error: method main(String[]) is already defined in class A
public static void main(String[] args) {
^
1 error
不定义包(package),写个类“A”,实现main方法输出Hello World。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
怎么编译前一步所写Java程序?
javac HelloWorld.java
怎么执行编译得到的Java字节码?
java HelloWorld
改写程序,把“A”类放到包(类名变为p.A)里,编译、执行
文件路径: p/A.java
package p;
public class A {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
在“A”类源代码(A.java)所在的目录里,另写个类“B”,在“B”类上添加个方法,由“A”类调用
p/A.java
package p;
import p.B;
public class A {
public static void main(String[] args) {
System.out.println("Hello World!");
B.hello(args);
}
}
class B {
public static void hello(String[] args) {
System.out.println("I am class B.");
}
}
什么是classpath?
类似于环境变量中的 PATH 变量, 告诉 jvm 需要搜索 class 文件的路径, 可通过参数 -cp, -classpath, --class-path
指定.
如 java -cp ./testJava p.A
, jvm 会到 当前目前下的 testJava 子路径下 p 文件夹下所搜 A.class 文件.
如果找不到会报错:
$ java p.A
Error: Could not find or load main class p.A
Caused by: java.lang.ClassNotFoundException: p.A
把“B”类,改变其包名,挪到“A”类所在目录外面去,做必要修改使程序能运行
文件路径为:
$ tree
.
├── p
│ ├── A.class
│ └── A.java
└── q
├── B.class
└── B.java
A.java
package p;
import q.B;
public class A {
public static void main(String[] args) {
System.out.println("Hello World!");
B.hello(args);
}
}
B.java
package q;
public class B {
public static void hello(String[] args) {
System.out.println("I am class B.");
}
}
什么是Jar?
Jar 文件就是打包的 class 文件, 并且可以保持层级结构. 本质就是 zip 压缩包.
怎么把代码打包成Jar?
- 如果不包含 manifest 文件:
$ jar -cf first.jar p/A.class q/B.class
(会使用默认的 manifest 文件) - 不从 manifest 文件指定入口类:
jar -cvfe first.jar p.A p/A.class q/B.class
- 如果包含 manifest 文件:
jar -c --manifest manifest.mf -f first.jar p/A.class q/B.class
manifest.mf 文件内容Manifest-Version: 1.0 Main-Class: p.A
- 将指定文件夹中的文件全部打包进 jar 文件:
$ jar --manifest manifest.mf -c -f first.jar -C ./ .
怎么执行一个Jar?
- 如果 Jar 文件中没有指定 Main-Class, 可以这样执行:
$ java -cp code.jar p.A Hello World! I am class B.
- 如果 Jar 文件中指定了 Main-Class, 可以这样执行:
java -jar first.jar
执行 .java .class 文件的方法(带有依赖)
总原则:
- 如果有依赖,可以通过 -cp 指定依赖的 jar包,可以使用 *.jar 通配符;
- 执行单个 .java .class .jar(没有指定主函数时), java 会从 -cp 中的第一个文件中寻找主函数,因此要将主函数所在文件放到 -cp 的第一个;
guang@pc77:~/projects/demo_java/src/main/java/xintek
$ java Jdbc.java -cp /home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/mysql-connector-java-8.0.30.jar
guang@pc77:~/projects/demo_java/src/main/java
java -cp /home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/*.jar Jdbc.java
java -cp ./:/home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/* Jdbc.java
guang@pc77:~/projects/demo_java/src/main/java
$ java -cp ./:/home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/mysql-connector-java-8.0.30.jar xintek.Jdbc
guang@pc77:~/projects/demo_java/src/main/java
$ java -cp ./:/home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/* xintek.Jdbc
guang@pc77:~/projects/demo_java
$ java -cp target/demo_java-1.0.jar:/home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/* xintek.Jdbc
guang@pc77:~/projects/demo_java
$ java -cp target/demo_java-1.0.jar:/home/guang/.m2/repository/mysql/mysql-connector-java/8.0.30/mysql-connector-java-8.0.30.jar xintek.J
dbc
Maven 操作
// (四)先不用IDE,用Maven重做(三)
用archetype创建项目
mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
备注:
- 如果将
DarchetypeVersion
去掉, 或者将其设置为其他值, 都会遇到以下报错:
备注: DarchetypeVersion: 表示模板的版本. 1.4 表示其版本号.[ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] Source option 5 is no longer supported. Use 6 or later. [ERROR] Target option 1.5 is no longer supported. Use 1.6 or later.
-
-D
: 表示通过 maven 向 archetype:generate 传参. -
groupId, artifactId
: archetype:generate 的参数.
使用mvn编译项目
- compile: 将 .java 文件编译成 .class 文件
- package: 将 .class 文件及需要的配置文件打包成 .jar 文件
- install: 将打包成的 .jar 文件进行本地安装. 其他项目就可以直接调用.
使用 mvn 运行项目
java -cp target/classes/ com.mycompany.app.App
对于非可执行 jar 文件, 需要指定 Main-Class:
java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App
: 手动指定 Main-Class..
如果不指定 Mian-Class 会报错: no main manifest attribute, in target/my-app-1.0-SNAPSHOT.jar
对于可执行 jar 文件, 不需要再次指定:
直接执行 jar 包: java -jar target/my-app-1.0-SNAPSHOT.jar
使用 maven 制作可执行 jar 包的方法(即指定 package 时指定 Main-Class):
- 向
pom.xml
文件添加以下内容:<plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>主函数路径</mainClass> </manifest> </archive> </configuration> </plugin>
- 重新打包:
mvn clean package
- 执行jar文件:
java -jar target/my-app-1.0-SNAPSHOT.jar
列出所有依赖的 classpath
mvn dependency:build-classpath
查看当前项目生效的 POM 配置
mvn help:effective-pom
查看当前生效的配置
mvn help:effectice-settings
制作包含依赖的可执行 jar 包
-
向 pom.xml 文件中添加:
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>主函数路径</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>
如果去掉 <archive></archive>片段,生成的是非可执行jar包。
-
编译项目:mvn clean package assembly:single
- 会生成2个jar包:
- demo_java-1.0.jar: 不包含依赖的 jar包;
- demo_java-1.0-jar-with-dependencies.jar 的可执行 jar 包(包含依赖的 jar与主函数)。
- 会生成2个jar包:
正则匹配
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public static void main(String[] args) {
String content = "I'm Bob, my phone is 123";
Pattern pattern = Pattern.compile("(\\d*)");
Matcher result = pattern.matcher(content);
if (result.find()) {
System.out.println(result.group(1));
}
}
逐行读取/写入文件
import java.io.*;
import java.nio.charset.Charset;
public static void main(String[] args) throws IOException {
File fr = new File("target/classes/first.txt");
File fw = new File("target/classes", "second.txt");
if (fr.exists()) {
BufferedReader br = new BufferedReader(new FileReader(fr));
// 第一个参数表示文件路径; 第二个参数表示编码格式, UTF8, GBK; 第三个参数表示是否追加, true: a, false(默认): w
BufferedWriter bw = new BufferedWriter(new FileWriter(fw, Charset.forName("UTF8"), true));
for (String line; (line = br.readLine()) != null;) {
System.out.println(line);
bw.append(line);
bw.newLine();
}
br.close();
bw.close();
}
}
备注:可使用 try 语句实现文件的打开与自动关闭。
将 byte[] 类型转换成字符串:new String(byte[], StandardCharsets.UTF_8)
.
逐行读取 gzip 文件
try (BufferedReader br = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(filePath.toFile())), StandardCharsets.UTF_8))
) {
for (String line; (line = br.readLine()) != null; ) {
System.out.println(line);
}
}
Json 序列化与反序列化
https://www.baeldung.com/java-org-json
重载
要求: 形参列表不同
- 参数个数不同;
- 参数类型不同;
方法重载与形参名称, 权限修饰符(public, private等), 返回值类型无关.
可变参数个数方法
int sum(int... nums) 等价于 int sum(int[] nums).
可以有 0, 1 .. 个参数.
应用场景:
String sql = "update customers set name = ?, email=? where id=?";
String sql = "update customers set name = ? where id=?";
public void update(String sql, object... objs)
值传递
形参: 定义方法时, 参数列表中的变量;
实参: 调用方法时, 参数列表中的变量;
Java 中调用方法时参数的传递方式: 值传递.
- 基本数据类型, 传递变量值;
- 应用数据类型, 传递变量地址(2侧会同步改动);
静态字段和静态方法
使用 static 修饰;
静态字段
所有变量共享一个静态字段, 可以使用 instance.field 或 class.field(推荐方法);
interface 是一个纯抽象类, 不能定义实例字段. 但是 interface 可以静态字段, 并且必须是 final static.
public interface Person{
public final static int MALE = 1;
public final static int FEMALE = 2;
}
因为 interface 只能定义 public final static 类型的字段, 所以可以省略 public final static, 编译器会自动加上 public final static.
public interface Person{
int MALE = 1;
int FEMALE = 2;
}
静态方法:
特点:
- class.method(), 即 类名.静态方法();
- 静态方法内部只能访问静态字段, 无法访问实例字段;
- 程序入口 main 也是 静态方法;
- 常用用于工具类, 如 Array.sort();
继承
重写 override
@override 修饰符只是起校验的作用, 并影响该方法是否是重写, 也可以不写.
- 方法名, 形参列表必须相同.
- 子类中该方法的权限>=父类中该方法的权限, 权限由大到小: public > package > protect > private.
- 子类无法重写父类中 private 类型的方法.
- 返回值类型:
- 如果父类的返回值类型是 void 或者基本数据类型, 那么子类必须和父类一致;
- 如果父类的返回值类型是 应用型类型, 那么子类需要是相同类型, 或者是其子类;
- 抛出的异常: 子类需要相同类型异常, 或者其异常类型的子类.
多态
多态是指程序在编译和执行时表现出不同的行为.
先决条件:
- 父类和子类;
- 子类重写了父类的某些方法;
因此需要有以下假如条件: 有 2 个 class: Person, Man.
其中 Man 都继承于 Person. 并且重写了方法: walk, 同时有自己的专有方法: isSmoke.
Person a = new Man();
a.walk;
a.id;
在编译时, a.walk, a.id 调用的都是 Person 类的方法和属性.
但是在运行时, a.walk 调用的是 Man 类的 walk 方法, Person 类的属性.
这种调用父类方法, 但在执行时调用的却是子类方法, 叫虚方法调用(Virtual Method Invocation). 调用属性时不受影响.
优点
降低代码冗余.
如果有多个子类继承父类, 如 Woman, Girl, Boy 等继承 Person, 并且都重写了 walk 方法.
需要测试(或其他操作) walk 时, 可以直接将被测试对象定义为 Person 类, 并且调用 Person.walk(). 这样实际使用时, 可根据需要传入 Man, Woman 等类, 不会出现语法错误, 实际调用时, 调用的也就是 Man.walk, Woman.walk. 这就不用再对 Man, Woman 类单独写类似的代码了.
在开发中: 使用父类作为方法的形参, 是多态使用最多的场景. 即便增加了新子类, 也无需修改代码. 提高了拓展性.
开闭性: 对拓展功能开放, 对修改代码关闭.
缺点
在定义Person a = new Man()
时, 会在内存中定义一个 Man 类型的实例(具有 Man 类所有的属性和方法), 但是该实例却不能调用 Man 类专有的方法, 如 isSmoker().
向上转型/向下转型
[图片上传失败...(image-785821-1687163467119)]
多态就是向上转型.
刚才提到的"多态"的缺点, 可以向下转型 Person -> Man 之后, 再调用 .isSmoker() 方法.
Person p = new Man();
Man m = (Man) p;
m.isSmoker();
这里可能会有一个问题, 如果 Person 类还有一个子类 Woman, 并且该类没有 isSmoker() 方法.
Person p = new Woman();
if (p instanceof Man){
Man m = (Man)p;
m.isSmoker();
}
强制转换时, 需要先验证类型, 再转换, 否则调用专有方法时, 如果类不匹配, 就会报错(ClassCastException).
虽然 p 声明是 Person, 但使用 instanceof 判断时, 却是 Woman.
常用方法
finalize()
GC 要回收该对象时, 会执行该方法(JDK9 之后就建议不再使用)
将一个变量执行 null 时, 该变量之前指向的变量就可以被 GC 回收了.
Person p = new Person();
p = null;
代码块
抽象类 抽象方法
abstract 不能与以下关键词共用:
- private: private 类型的方法不能被子类重写, 抽样方法要求必须要重写;
- static: 静态方法可以被类调用, 抽样类不能被类/实例调用;
- final: final 修饰的 类/方法 不能被重写.
抽象类
abstract 修饰 类
- 抽样类不能进行实例化.
- 可以包含构造器;
- 可以没有抽象方法. 如果有抽象方法, 就一定是抽象类.
抽象方法
abstract 修饰 方法
- 包含抽样方法的类必须是抽样类.
- 子类必须重写/实现父类中所有的抽样方法, 否则该子类也必须是抽象类.
接口
接口是一种规范, 实现了某个接口, 就是具有了该功能. 如笔记本使用了 Type-C 接口, 那么该笔记本就具备了相应的功能.
类是表示范围大小, 关系的从属.
可以用于声明
- 属性: 必须使用 public static final 修饰, 因此可以将 public static final 省略;
- 方法: JDK8 之前必须可以使用 public abstract 修饰, 因此可以将 publish abstract 省略;
- JDK8 之后可以使用 default 修饰, 实现类不要实现 default 修饰的抽象方法.
不可以以用于声明
除 属性和方法之外的, 如 构造器, 代码块.
格式
class A extends SupserA implements B, C {}
- A 是 SuperA 的子类;
- A 是 B, C 的实现类;
接口与接口的关系
接口可以继承另一个接口, 并且运行多继承. 如:
interface A {}
interface B {}
interface C extends A, B{} # C 中会自动包含 A, B 中所有的方法和属性;
class D implements C{} # 需要重写/实现 A, B, C 中所有的方法
接口的多态性
和类的多态性类似: 接口I 变量i = new 类C;
其中: 类名C 实现了 接口I. 变量i只能调用 接口I 中定义的方法.
编译是 变量i 属于接口I, 但是运行时, 却是属于 类C.
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
List 是接口, ArrayList 是实现类. 变量 list 只能调用 接口List 的抽象方法. 变量 coll 也只能调用 接口Collection 的抽象方法.
注意事项
- 类可以实现多个接口(如果不能实现该接口中所有的方法, 那么该类只能定义为抽象类, 因为接口中定义的方法是抽象方法).
- 一定程度上弥补了类的单继承的限制.
public class Main {
public static void main(String[] args) {
Person p = new Student("Xiao Ming");
p.run();
}
}
interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
实现类不需要实现 default 修饰的抽象方法, default 方法的目的: 当我们给一个接口新增一个方法时, 需要修改所有涉及的实现类. 如果新增的是 default 方法, 子类就不需要全部修改, 可以根据需要选择性地重写.
- 如某个版本新增一个方法, 可以先定义为 default 类型, 并发出 deprecated 警告, 并在某个后续版本中将 default 类型改为 public abstract 类型.
抽象类和接口
区别点 | 抽象类 | 接口 |
---|---|---|
定义 | 可以包含抽象方法的类 | 主要是全局常量和抽象方法的集合 |
组成 | 构造方法, 抽象方法, 普通方法, 常量, 变量 | 常量, 抽象方法 |
使用 | 子类继承抽象类 | 子类实现接口 |
关系 | 抽象类可以实现多个接口 | 接口不能继承抽象类, 可以继承多个接口 |
常见设计模式 | 模版方法 | 简单工厂, 工厂方法, 代理模式 |
对象 | 都可以通过对象的多态性产生实例化对象 | |
局限 | 单继承 | 多个多继承 |
实际 | 作为模版 | 作为一种标准, 表示一中功能 |
选择 | 如果抽象类和接口都可以实现, 优先使用实现接口, 可以突破单继承的限制. |
Lambda 表达式
- () -> 5
- x -> 2 * x
- (x, y) -> x – y
- (int x, int y) -> x + y
- (String s) -> {System.out.print(s);}
如果使用外部变量,该变量需要 不可修改(即被 final 修饰):
final int num = 1;
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
包
包没有父子继承关系, java.util 和 java.utils.zip 是不同的包, 两者没有任何继承关系;
处于同一个包的类, 可以访问包作用域内的字段和方法. 不使用 public, protected, private 等修改的字段和方法就是包作用域.
引入包的几种方法:
- 不使用 import 引入, 使用时写成完整类名: java.util.Arrays;(比较麻烦)
- 使用 import 引入某个类名, import java.util.Arrays;(推荐)
- 使用 import 引入某个类的全部子类, import java.util.*;(不推荐)
- 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!"); } }
类名查找顺序
- 如果是完整类名, 就根据完整类名查找该类;
- 如果是简单类名:
- 当前 package; (会自动引入当前 package 内的所有类)
- import 的包是否包含该类;
- java.lang 是否包含该类; (会自动引入 java.lang)
- 无法依然无法找到, 就会报错;
javac -d ./bin src/*/.java: 会编译 src 文件夹下所有的 .java 文件, 包括任意深度的子文件夹.
win 下不支持该语法 src/*/.java, 所以需要列出所有的 .java 文件.
作用域
访问权限修饰符
修饰符有: public, package(default), protected, private
public
- 修饰类和接口时: 该类可以被任何类访问 (一个文件中最多有一个该类);
- 修饰字段和方法时: 可以被任何类访问, 前提: 有所属类的访问权限;
package
没有 public, protected, private 等修饰的类, 方法, 字段都是 package 类型的.
- 修饰类时:可以被当前包内的所有类访问;
- 修饰方法和属性时:可以被当前包内所有的类访问;
注意:
- 包名必须完全一致, com.apache 和 com.apache.abc 不是一个包.
protected
只能修饰 属性、方法;
- 访问的前提:可以访问所属类;
- 可以被当前包内的所有类访问;
- 可以被子类(子类的子类等)访问;
private
只能在当前类和嵌套类内使用(与方法声明的顺序无关).
- 只能被当前类和当前类的嵌套类访问;
推荐将其他类型(如 public 等)的方法放在前面, 应当首先关注, private 的方法放在后面.
嵌套类: 在当前类的实现中, 又定义了其他类.
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}
// private方法:
private static void hello() {
System.out.println("private hello!");
}
// 静态内部类:
static class Inner {
public void hi() {
Main.hello();
}
}
}
final
- 不属于访问权限修饰符.
- 修饰 class, 可以防止该类被继承;
- 修饰 method, 可以防止该方法被覆写 (override);
- 修改 field 或 变量, 可以防止被重新赋值;
常用数据类型
- 基本类型: byte,short,int,long,boolean,float,double,char;
- 引用类型: 所有 String class, array, interface 类型;
引用类型可以赋值为 null, 但是基本类型不能赋值为 null;
基本类型存储的是该变量的值, 应用类型存储的是对应的地址.
赋值时都是值传递, 即基本类型传递数值, 应用类型数据地址(2者指向相同.)
String s = null;
int n = null; // compile error!
基本类型 | 对应的引用类型 |
---|---|
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 |
按照语义编程,而不是针对特定的底层实现去“优化”。
例如: 使用 == 比较 2 个同样大小的 Integer.
- 当数据较小时, 返回 true; 因为为了节省内存, 对于较小的数值, 始终返回相同的实例;
- 当数据较大时, 返回 false;
比较 2 个 Integer 类型的变量时, 要使用 equals(), 而非 ==. 绝不能因为Java标准库的 Integer 内部有缓存优化就使用 ==.
三目运算符
(cond)?exp1:exp2
如果 cond 为 true, 就取 exp1, 否则就取 exp2.
exp1, exp2 既可以是变量, 也可以是表达式.
Boolean 布尔型
只有 true, false. 不能用 1, 非1(如0) 表示 boolean.
String
String 是引用类型, 并且具有不可变性 (因为内部通过 private final 做了限定).
比较大小
要使用 equals() 或 equalsIgnoreCase() 比较大小, 不能使用 == .
- 只有 2 个 String 引用的是同一个对象时, 使用 == 比较时, 才是 true;
- 使用 equals() 或 equalsIgnoreCase() 做对比时, 只要 2 个对象的值相同即可;
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
类型转换
转换成字符串
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
字符串和二进制编码的转换
byte[] b = "hello".getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.toString(b));
String b_str = new String(b, StandardCharsets.UTF_8);
System.out.println(b_str);
StringBuilder
该数据类型线程不安全. String 类型线程安全.
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
虽然可以这样直接拼接字符串, 但是每次循环中都会创建新字符串对象, 然后扔掉旧字符串, 这样大部分字符串都是临时对象, 不仅浪费内存, 还会相应GC效率.
为了高效拼接字符串, java 标准库提供了 StringBuilder, 它是可变对象, 可以预分配缓冲区, 这样往 StringBuiler 对象新增字符时, 不会创建新的临时对象.
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
// 可以使用链式操作
sb.append(',').append(i);
}
String s = sb.toString();
备注:
- 对于普通的字符串 + 操作, 不需要将其改写为 StringBuilder, Java 编译器在编译时会自动将多个 + 操作优化成 StringConcatFactory 操作, 在执行期间, StringConcatFactory 会自动把字符串连接操作优化为数组复制或 StringBuilder 操作.
- 之前的 StringBuffer 是 StringBuilder 的线程安全版本, 属于 Java 的早期版本, 现在很少使用.
StringJoiner
使用分隔符拼接数组的需求很常见, 可以使用 StringJoiner 解决.
String[] names = { "Bob", "Alice", "Grace" };
StringJoiner sj = new StringJoiner(",", "hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
日期 时间
获取当前时间的 Unix 时间戳
Instant now = Instant.now()
常用转换
// 获取当前时间
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
// 将 Unix 时间戳转换成 datetime 类型
Instant ins = Instant.ofEpochSecond(1683979214);
ZonedDateTime zdt = ins.atZone(ZoneId.of("UTC"));
ZonedDateTime zdt = ins.atZone(ZoneId.of("Asia/Shanghai"));
// 使用系统时区
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
// 计算 datetime 对应的 Unix 时间戳
long timestamp = zdt.toEpochSecond();
System.out.println(timestamp);
// 将 datetime 类型按照指定格式转换成字符串
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter.format(zdt));
// 将特定格式的字符串解析为 Datetime
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
// Localdatetime 与 ZonedDatetTime 之间的转换
LocalDateTime ldt = LocalDateTime.of(2019, 9, 15, 15, 16, 17);
ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault());
ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York"));
LocalDateTime ldt = zbj.toLocalDateTime();
// 同一时间在不同时区之间转换
// 以中国时区获取当前时间:
ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 转换为纽约时间:
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
异常处理
断言
assert x >= 0 : "x must >= 0";
如果前面的断言条件 为 False, 就会抛出 AssertionError, 并带上 "x must >= 0" 信息.
抛出 AssertionError 会导致程序退出. 因此. 断言不能用于可恢复的程序错误(应该使用"抛出异常"), 只应该应用在开发和测试阶段.
JVM 会默认关闭断言指令, 跳过断言语句. 如果要执行断言, 需要给 JVM 传递参数 -enableassertions (简写 -ea), 如: java -ea Main.java
.
多线程
创建子线程
- 从Thread派生一个自定义类,然后覆写run()方法
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 创建 Thread 实例时,传入一个Runnable实例
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 使用 lambda 表达式
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
- 使用匿名类
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
等待子进程结束
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
// 会等待子进程 t 结束再向下执行
t.join();
System.out.println("end");
}
}
在 JVM 中所有的变量都保存在 主内存中,子线程访问时,需要先将该变量复制到自己的工作内存。这样,多个线程同时使用一个变量时,可能会有冲突。可以使用 volatile 关键词,有2个作用:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改后,立刻会写至主内存;
X86 框架,JVM 会写内存速度很快,ARM 架构下就会有显著的延迟。
线程安全
锁住的是当前实例 this,创建多个实例时,又不会相互影响。
public class Counter {
private int counter = 0;
public synchronized void add(int n) { // 锁住this
count += n;
}
// 与上面语句等价
// public void add(int n) {
// synchronized (this) {
// counter += n;
// }
// }
public synchronized void dec(int n) { // 锁住this
count -= n;
}
// public void dec(int n) {
// synchronized (this) {
// counter -= n;
// }
// }
public int get() {
return counter;
}
}
现成安全的类:
- java.lang.StringBuffer;
- String, Interger, LocalDate 所有成员变量都是 final,所以也线程安全。
- Math 只提供静态方法,没有成员变量,也是线程安全。
可重入锁
JVM 允许同一个线程重复获取同一把锁,这种锁叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
日志
通常使用 org.apache.commons.logging 模块记录日志. 可以通过以下 2 种方法:
// 1. 在静态方法中引用 Log:
public class Main {
static final Log log = LogFactory.getLog(Main.class);
static void foo() {
log.info("foo");
}
}
// 2. 在实例方法中引用 Log:
public class Person {
protected final Log log = LogFactory.getLog(getClass());
void foo() {
log.info("foo");
}
}
第一种方式定义的 log 在静态方法和实例方法中都可以使用;
第二种方式定义的 log 可以用于继承中, 子类的 log 会自动根据 getClass() 判断类名, 但是只能在实例方法中使用;
Java 中记录日志一般使用 "日志 API + 底层实现" 的方式.
日志 API
日志 API 主要是对底层实现进行封装, 对外提供统一的调用接口.
常用的有: apache.common-logging, SLF4j;
common-logging 的 jar 包:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
SLF4J 的 jar 包:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
底层实现
不同的底层实现有不同的功能和性能, 常用的有 log4j, logback. 配置文件分别为:log4j2.xml, logback.xml. 推荐将配置文件放到 src/main/resources 文件夹内, 这样使用 maven 编译后, 配置文件的路径为 target/classes (class 文件对应的 classpath).
log4j 的 jar 文件:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
logback 的 jar 包:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
common-logging + log4j
- common-logging 的 jar 包;
- log4j 的 jar 包;
- common-logging 与 log4j 之间的连接器
common-logging 与 log4j 之间的连接器:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jcl</artifactId>
<version>2.19.0</version>
</dependency>
如果 common-logging 没有在 classpath 中发现 log4j 和 连接器的 jar 包, 就会自动调用内置的 java.util.logging; 如果发现了, 就会自动调用 log4j 作为底层.
SLF4J + LOG4J
slf4j 对应的 jar 包:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.6</version>
</dependency>
slf4j 与 log4j 的连接器的 jar 包:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>