java 随笔

2023-06-18  本文已影响0人  Guang777

管理 Java 多个版本

  1. sudo update-alternatives --config java
  2. sudo update-alternatives --config javac

(一)在Linux系统里

什么是环境变量?

系统级或用户级的变量. 类型与程序/脚本中的变量, 只不过作用域是整个系统或当前用户.
/etc/profile.d/对所有用户都有效.

~/.bashrc: 只对当前用户有效

环境变量PATH的作用?

当输入命令 grep 等不完整路径的命令时, 系统处理会在当前路径下搜索该程序, 还会到 PATH 中的路径进行搜索. 如: /usr/bin

怎么修改PATH?怎么持久化这个修改(避免被重置)?

临时修改: export PATH=$PATH:your_path
持久化修改:

  1. /etc/environment 中修改; 影响所有用户.
  2. /etc/profile.d/ 中创建相应的 bash 脚本, 影响所有用户.
  3. ~/.bashrc 中修改, 只影响当前用户.
  4. 重启系统或 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 + 核心类库

核心类库需要举例

什么是JDK?请说明JDK和JRE的关系。

JDK: JRE + 开发工具(编译器 javac, jdb, jar)

什么是JDK发行版?请举二例。

JDK 的不同实现. 源码相同, 构建方式不同.

备注: OpenJDK 不属于发行版, 类似于 Linux 与 Ubuntu.

[图片上传失败...(image-19a79c-1687163467119)]

// (三)先只使用JDK、命令行,不用Maven、IDE等工具……

不使用包管理器,手动安装JDK 11

  1. 下载 jdk 文件 下载链接
  2. 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?

怎么执行一个Jar?

  1. 如果 Jar 文件中没有指定 Main-Class, 可以这样执行:
    $ java -cp code.jar p.A
    Hello World!
    I am class B.    
    
  2. 如果 Jar 文件中指定了 Main-Class, 可以这样执行:
    java -jar first.jar

执行 .java .class 文件的方法(带有依赖)

总原则:

  1. 如果有依赖,可以通过 -cp 指定依赖的 jar包,可以使用 *.jar 通配符;
  2. 执行单个 .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

备注:

使用mvn编译项目

使用 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):

  1. pom.xml文件添加以下内容:
    <plugin>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
            <archive>
            <manifest>
                <mainClass>主函数路径</mainClass>
            </manifest>
            </archive>
        </configuration>
    </plugin>  
    
  2. 重新打包: mvn clean package
  3. 执行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 包

  1. 向 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包。

  2. 编译项目:mvn clean package assembly:single

    1. 会生成2个jar包:
      1. demo_java-1.0.jar: 不包含依赖的 jar包;
      2. demo_java-1.0-jar-with-dependencies.jar 的可执行 jar 包(包含依赖的 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

重载

要求: 形参列表不同

  1. 参数个数不同;
  2. 参数类型不同;

方法重载与形参名称, 权限修饰符(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 中调用方法时参数的传递方式: 值传递.

静态字段和静态方法

使用 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;
}

静态方法:

特点:

继承

重写 override

@override 修饰符只是起校验的作用, 并影响该方法是否是重写, 也可以不写.

  1. 方法名, 形参列表必须相同.
  2. 子类中该方法的权限>=父类中该方法的权限, 权限由大到小: public > package > protect > private.
  3. 子类无法重写父类中 private 类型的方法.
  4. 返回值类型:
    1. 如果父类的返回值类型是 void 或者基本数据类型, 那么子类必须和父类一致;
    2. 如果父类的返回值类型是 应用型类型, 那么子类需要是相同类型, 或者是其子类;
  5. 抛出的异常: 子类需要相同类型异常, 或者其异常类型的子类.

多态

多态是指程序在编译和执行时表现出不同的行为.

先决条件:

因此需要有以下假如条件: 有 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 不能与以下关键词共用:

抽象类

abstract 修饰 类

抽象方法

abstract 修饰 方法

接口

接口是一种规范, 实现了某个接口, 就是具有了该功能. 如笔记本使用了 Type-C 接口, 那么该笔记本就具备了相应的功能.

类是表示范围大小, 关系的从属.

可以用于声明

不可以以用于声明

除 属性和方法之外的, 如 构造器, 代码块.

格式

class A extends SupserA implements 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 方法, 子类就不需要全部修改, 可以根据需要选择性地重写.

抽象类和接口

区别点 抽象类 接口
定义 可以包含抽象方法的类 主要是全局常量和抽象方法的集合
组成 构造方法, 抽象方法, 普通方法, 常量, 变量 常量, 抽象方法
使用 子类继承抽象类 子类实现接口
关系 抽象类可以实现多个接口 接口不能继承抽象类, 可以继承多个接口
常见设计模式 模版方法 简单工厂, 工厂方法, 代理模式
对象 都可以通过对象的多态性产生实例化对象
局限 单继承 多个多继承
实际 作为模版 作为一种标准, 表示一中功能
选择 如果抽象类和接口都可以实现, 优先使用实现接口, 可以突破单继承的限制.

Lambda 表达式

如果使用外部变量,该变量需要 不可修改(即被 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 等修改的字段和方法就是包作用域.

引入包的几种方法:

类名查找顺序

  1. 如果是完整类名, 就根据完整类名查找该类;
  2. 如果是简单类名:
    1. 当前 package; (会自动引入当前 package 内的所有类)
    2. import 的包是否包含该类;
    3. java.lang 是否包含该类; (会自动引入 java.lang)
  3. 无法依然无法找到, 就会报错;

javac -d ./bin src/*/.java: 会编译 src 文件夹下所有的 .java 文件, 包括任意深度的子文件夹.

win 下不支持该语法 src/*/.java, 所以需要列出所有的 .java 文件.

作用域

访问权限修饰符

修饰符有: public, package(default), protected, private

public

package

没有 public, protected, private 等修饰的类, 方法, 字段都是 package 类型的.

注意:

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

常用数据类型

引用类型可以赋值为 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.

比较 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() 比较大小, 不能使用 == .

"".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();

备注:

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.

多线程

创建子线程

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!");
    }
}

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!");
    }
}

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;
    }
}

现成安全的类:

可重入锁

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

  1. common-logging 的 jar 包;
  2. log4j 的 jar 包;
  3. 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>
上一篇下一篇

猜你喜欢

热点阅读