java 随笔

管理 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 + 核心类库

核心类库需要举例

  • 类库: 多个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

  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?

  • 如果不包含 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?

  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

备注:

  • 如果将 DarchetypeVersion去掉, 或者将其设置为其他值, 都会遇到以下报错:
    [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.
    
    备注: DarchetypeVersion: 表示模板的版本. 1.4 表示其版本号.
  • -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):

  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 中调用方法时参数的传递方式: 值传递.

  • 基本数据类型, 传递变量值;
  • 应用数据类型, 传递变量地址(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 修饰符只是起校验的作用, 并影响该方法是否是重写, 也可以不写.

  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 不能与以下关键词共用:

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

类名查找顺序

  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 类型的.

  • 修饰类时:可以被当前包内的所有类访问;
  • 修饰方法和属性时:可以被当前包内所有的类访问;

注意:

  • 包名必须完全一致, 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

  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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容