Loading

Java学习笔记1-JavaSE

其他

通用的版本

jdk8(jdk1.8)

Java程序运行机制

  • 编译型
  • 解释型

Java程序运行机制.png

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

作用域修饰符

  • public
  • protected
  • private
  • package: 不加以上修饰符的类或方法,可以被同一个package下的类访问
  • 局部变量:方法内部的变量
  • final:
    • 修饰class阻止被继承
    • 修饰method阻止被子类覆写
    • 修饰field局部变量阻止被重新赋值

一个.java文件只能包含一个public类,但可以包含多个非public类。

编译时查找类的机制

编译器在生成calss的时候,会默认自动

  • import当前package的其他class
  • import java.lang.*

所以当编译器遇到一个class名称时,

  • 如果是完整类名,直接查找对应的class
  • 如果是简单类名,会按照下面的顺序依次查找:
    1. 查找当前package是否存在这个class
    2. 查找import的包是否包含这个class
    3. 查找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,设置如下:
Java-partice-1.png

classpath 和 jar

classpathJVM用到的一个环境变量,它用来指示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 1155
java 1761
  • 低版本jdk不兼容高版本class(高版本class文件无法在低版本的jre环境下执行):
    • 如果Java 17编译一个Java程序,输出的class文件版本默认就是61,它可以在Java 17Java 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));
    }
}

编译并运行模块:

  1. 编译所有.java文件
java- d bin /src/modeule-info.java src/com/itranswarp/sample/*.java
  1. 所有class打包成hello.jar文件,传入--main-class,让这个jar包能自己定位main方法所在的类:
jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .
  1. hello.jar包转换模块
jmod create --class-path hello.jar hello.jmod
  1. 运行模块 (module-path指定的模块不是jmod后缀,那是否可以跳过第3步?)
java --module-path hello.jar --module hello.world
  1. 编译模块(真)打包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.xmlmodule-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 反编译的功能

循环结构

  • while
  • do while
  • for //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 classinterface
继承只能 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;
    } 
}

基本类型对应的包装类型:

基本类型对应的引用类型
booleanjava.lang.Boolean
bytejava.lang.Byte
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double
charjava.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是异常体系的根,有两个体系:ErrorException

  • Error处于严重的错误,程序对此一般无能为力,例如:
    • OutOfMemoryErro: 内存耗尽
    • NoClassDefFoundError: 无法加载某个Class
    • StackOverflowError: 栈溢出
  • Exception则是运行时的错误,它可以被捕获处理:
    • 应用程序逻辑处理的一部分,需要被捕获并处理,例如:
      • NumberFormatException: 数值类型的格式错误
      • FileNotFoundException: 未找到文件
      • SocketException:读取网络失败
      • UnsupportedEncodingException
    • 程序逻辑编写不对造成的bug,需进行修复
      • NullPointerException:
      • IndexOutOfBoundsExcepton: 数组索引越界

Exception 又分为两大类:

  • RuntimeException 以及子类
  • RuntimeException(包括IOExceptionReflectiveOperationException等等)

Java规定:

  • 必须捕获的异常,不包括RuntimeExceptionException及其子类的异常,被称为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导入第三方包

Commons Logging下载地址

  1. 创建lib文件夹(lib文件夹位于项目根目录,与.idea目录同级),将第三方jar包(commons-logging-1.3.3.jar)复制到lib文件夹下
  2. 对lib文件建立依赖:选择【文件】-【项目结构】,在此页面导入lib文件夹下的第三方jar包,如下图所示
    java-import-jar.png

IDEA

常用类

集合框架

Maven 基础

IO

多线程

网络编程

注册和反射

JUC编程

JVM探究

【扩展】23种设计模式

[扩展]XML

【扩展】数据结构和算法

【扩展】正则表达式

参考

[狂神说]
笔记

posted @ 2025-04-03 11:19  一起滚月球  阅读(18)  评论(0)    收藏  举报