发新话题
打印

[开发] 《Java教程》廖雪峰

面向对象基础-模块

模块和jar包的区别是:

  • jar只是简单打包,模块需要写入以来关系(还可以包含JNI扩展)
  • 模块支持多版本(一个模块中可以为不同的JVM提供不同版本)


编写模块
src目录下新增module-info.java
代码:
module hello.world {
        requires java.base; // 可不写,任何模块都会自动引入java.base
        requires java.xml;
}
声明依赖关系后,可以引入模块
代码:
package com.itranswarp.sample;

// 必须引入java.xml模块后才能使用其中的类:
import javax.xml.XMLConstants;

public class Main {
        public static void main(String[] args) {
                System.out.println("引入javax.xml.XMLConstats");
        }
}
开始创建模块:
1. 编译
代码:
$ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java
2. 打包jar
代码:
$ jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .
3. 转成模块
代码:
$ jmod create --class-path hello.jar hello.jmod
hello.jmod就是打包出来的模块

运行模块
裁剪不需要的模块,生成定制JRE
代码:
$ jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/
找到jre目录,目录中是包含了hello.jmod的JRE,运行
代码:
jre/bin/java --module hello.world
访问权限
模块与模块之间的调用,需要在被调用模块的module-info.java中配置exports,然后再在调用模块的module-info.java中require
代码:
module hello.world {
    exports com.itranswarp.sample;

    requires java.base;
    requires java.xml;
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-字符串和编码

String
两种写法
代码:
String s1 = "Hello";
String s2 = new String(new char[]{'H','e','l','l','o'});
字符串不可变,因为private final char[]

字符串比较
必须用equals()方法,不能用==
忽略大小写比较用equalsIgnoreCase()方法

搜索、提取子串,是否包含子串
代码:
// 是否包含子串:
"Hello".contains("ll"); // true
//搜索子串
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
//提取子串
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
去除首尾空白字符
trim() 去除空格,\t,\r,\n等
strip() 比trim方法多一个去除中文的空格字符\u30000
字符串的操作返回的是新字符串,原字符串没有改变
代码:
//去除字符
"  \tHello\r\n ".trim(); // "Hello"
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
//判断字符是否空
"".isEmpty(); // true,因为字符串长度为0
"  ".isEmpty(); // false,因为字符串长度不为0
"  \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串
replace()方法 - 替换所有符合的子串
代码:
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
replaceAll()方法 - 正则替换
代码:
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
分割字符串
split()方法,传入的参数是正则
代码:
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
拼接字符串
join()方法,传入的参数是字符串,连接字符串数组
代码:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
格式化字符串
formatted()方法和format()方法
代码:
String s = "Hi %s, your score is %d!";
s.formatted("Alice", 80); //Hi Alice, your score is 80!
String.format("Hi %s, your score is %.2f!", "Bob", 59.5); //Hi Bob, your score is 59.50!
占位符%s(字符串),%d(整数),%x(16进制),%f(浮点数)

类型转换
String.valueOf()方法 - 任意类型转为字符串
代码:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
Integer.parseInt()方法 - 字符串转为int类型
代码:
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
Boolean.parseBoolean()方法 - 字符串转为boolean类型
代码:
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
注意:Integer.getInteger()方法是将系统变量转为Integer,并非字符串转换
代码:
Integer.getInteger("java.version"); // 版本号,11
转换为char[]
String和char[]相互转换,用toCharArray()方法,和new String()
代码:
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
传入数组到方法中时,在内部需对传入数组进行copy,防止外部变化造成的影响

字符编码
常见的是ASCII,GB2312,Unicode编码
其中UTF-8是基于Unicode的可变长度编码,去除英文字符编码开头的00
UTF-8容错能力强,它依靠高字节位来确定字符的长度
char和String的Unicode编码,转码时李荣getBytes()方法转为byte类型数组,然后使用new String转为String类型
代码:
byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
String s1 = new String(b1, "GBK"); // 按GBK转换
String s2 = new String(b2, StandardCharsets.UTF_8); // 按UTF-8转换
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-StringBuilder

Stringp拼接可以直接用+,但这种方式会生成新的字符串对象,在for循环中,为了避免资源浪费,使用StringBuilder来拼接字符串
StringBuilder是一个可变对象,可以预分配缓冲区,往StringBuilder中新增字符时,不会创建新的临时对象
代码:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
    sb.append(',');
    sb.append(i);
}
String s = sb.toString();
StringBuilder支持链式操作
代码:
var sb = new StringBuilder(1024);
sb.append("Mr ")
  .append("Bob")
  .append("!")
  .insert(0, "Hello, ");
System.out.println(sb.toString());
链式操作的关键时返回this

普通字符串连接(非for循环),无需使用StringBuilder,因为系统会自动把多个连续+操作,编码为StringConcatFactory的操作,运行期间StringConcatFactory会自动把字符串的连接优化为StringBuilder操作

另外StringBuffer操作时早期的线程安全版本,会造成执行速度下降,现在已经无需使用
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-StringJoiner

for循环中高效拼接字符串,用StringBuilder,但遇到分隔符时,往往还需要去掉最后一个元素的分隔符,这时我们可以用StringJoiner拼接带分隔符的字符串
代码:
String[] names = {"Bob", "Alice", "Grace"};
var sj1 = new StringJoiner(", "); //只有带分隔符的字串
var sj2 = new StringJoiner(", ", "Hello ", "!"); //带上开头和结尾
for (String name : names) {
    sj1.add(name);
    sj2.add(name);
}
System.out.println(sj1.toString());
System.out.println(sj2.toString());
String.join()
StringJoiner的简单版本,适合不带开头和结尾的情况:
代码:
String[] names = {"Bob", "Alice", "Grace"};
String s = String.join(", ", names);
System.out.println(s); //只有带分隔符的字串
System.out.println("Hello " + s + "!"); //带上开头和结尾
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-包装类型


  • 基本类型:byte, short, int, long, float, double, char, boolean
  • 引用类型:class, interface

引用类型可以赋值为null,基本类型不可以
代码:
String s = null;
int n = null; // 编译报错
包装类(Wrapper Class)
由于基本类型不是对象,不带属性也不能调用方法,因此在面向对象编程过程中有时需要使用基本类型的包装类来操作。
例如定义一个Interger类,使其只包含一个实例字段int,这样Integer类就是int的包装类
代码:
public class Integer {
    private int value;

    public Integer(int value) {
        this.value = value;
    }

    public int intValue() {
        return this.value;
    }
}
基本类型于包装类型互换
代码:
Integer n1 = null;
Integer n2 = new Integer(99);
int n3 = n2.intVaule();
每种基本类型都对应了一个包装类:

基本类型     对应的引用类型
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

内置包装类型的使用
代码:
 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");
自动装箱(Auto Boxing)
正常int和Integer转换
代码:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
编译器自动转换
代码:
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
只发生在编译阶段,会影响执行效率。同时如果包装类型为null时,直接拆箱到int会报错
代码:
Integer n = null;
int i = n; //报错,基本类型不能为null
仅在JDK>=1.5编译环境进行

不变类
包装类型都是不变类,使用final修饰符修饰类和类中的属性值
因此一旦创建Interger对象,该对象就是不变的
对两个Integer对象比较,需要用equals()比较,不能用==

Integer.valueOf() 可能会返回同一实例,因此相比而言比较节省资源(推荐):

  • 方法1:Integer n = new Integer(100);
  • 方法2:Integer n = Integer.valueOf(100); //推荐

创建新对象时,优先选用静态工厂方法而不是new操作符。

进制转换
字符串解析为整数:
代码:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
整数格式化为指定进制的字符串:
代码:
Integer.toString(100); // "100",表示为10进制
Integer.toString(100, 36); // "2s",表示为36进制
Integer.toHexString(100); // "64",表示为16进制
Integer.toOctalString(100); // "144",表示为8进制
Integer.toBinaryString(100); // "1100100",表示为2进制
数据存储和显示分离:

  • 使用System.out.println(n); 输出时,自动把整数格式化为10进制,然后以String输出并显示
  • 使用Integer.toHexString(n)时,则通过核心库自动把整数格式化为16进制String并返回


包装类型一些有用的变量
代码:
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)
所有整数和浮点数包装类型都继承自number,可以直接通过包装类型向上转型为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();
处理无符号整型(Unsigned)
Java中没有无符号整型,借助包装类型可以实现转换
例如,byte是有符号整型,范围是-128~+127,但如果把byte看作无符号整型,它的范围就是0~255。我们把一个负的byte按无符号整型转换为int:
代码:
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-JavaBean

一个class,如果其属性字段的读写方法符合以下规范,就被成为JaveBean
假设属性字段名为xyz,读方法getXyz()(boolean属性字段读方法为isXyz()),写方法setXyz()
代码:
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
// boolean读方法:
public boolean isChild()
// boolean写方法:
public void setChild(boolean value)

  • 一组对应的读方法(getter)和写方法(setter)成为属性(property)
  • 只有getter的属性称为只读属性
  • 只有setter的属性称为只写属性(不常见)

属性只需要定义getter和setter,不一定需要对应的字段。例如child只读属性定义如下:
代码:
public class Person {
    private int age;

    public int getAge() { return this.age; }
    public void setAge(int age) { this.age = age; }

    public boolean isChild() {
        return age <= 6;
    }
}
JavaBean的作用
主要用于传递一组数据,同时也便于IDE自动生成getter和setter方法

枚举JavaBean属性
使用核心库Introspector的getBeanInfo方法
代码:
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
    System.out.println(pd.getName());
    System.out.println("  " + pd.getReadMethod());
    System.out.println("  " + pd.getWriteMethod());
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-枚举类

Java中可以使用static final定义常量
代码:
public class Weekday(){
    public static final int SUN = 0;
    public static final int MON = 1;
    public static final int TUE = 2;
    public static final int WED = 3;
    public static final int THU = 4;
    public static final int FRI = 5;
    public static final int SAT = 6;
}
使用常量
代码:
if (day == Weekday.SAT || day == Weekday.SUN) {
    // TODO: work at home
}
使用时无法检查常量的合理性(如判断星期八)

enum
使用enum来定义枚举类
代码:
enum Weekday {
    SUN, MON, TUE, WED, THU, FRI, SAT;
}
优势:
1. enum带有类型信息(即Weekday.SUN类型为Weekday),编译器会自动检查出类型错误
代码:
int day = 1;
if (day == Weekday.SUN) { // Compile error: bad operand types for binary operator '=='
}
2. 不可能引用到非枚举的值(无法通过编译)
3. 不同类型的枚举不能相互比较或赋值(类型不符)
代码:
Weekday x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types
enum比较
使用enum定义的枚举类是一钟引用类型,但它可以使用==比较(例外,因为enum类型的每个常量在JVM里只有一个唯一实例)
代码:
if (day == Weekday.FRI) { // ok!
}
if (day.equals(Weekday.SUN)) { // ok, but more code!
}
enum类型
通过enum定义的枚举类,和其他class没有区别,特点:

  • enum类型继承自java.lang.Enum类,并且无法被继承(final)
  • 可以定义enum的实例,无法new
  • 定义的每个实例都是引用类型的唯一实例
  • 可以将enum用于switch
代码:
public enum Color {
    RED, GREEN, BLUE;
}
每个枚举值都是enum类的实例,因此可以使用一些方法:
name() - 返回常量名
代码:
String s = Weekday.SUN.name(); // "SUN"
ordinal() - 返回常量顺序,从0开始
代码:
int n = Weekday.MON.ordinal(); // 1
改变定义的enum中枚举的顺序,会改变ordinal()的返回值,所以必须保持新增的常量在最后

定义枚举值附加字段
如果要给每个枚举值添加字段,可以自定义构造方法
代码:
public class Main {
    public static void main(String[] args) {
        Weekday day = Weekday.SUN;
        if (day.dayValue == 6 || day.dayValue == 0) {
            System.out.println("Work at home!");
        } else {
            System.out.println("Work at office!");
        }
    }
}

enum Weekday {
    MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);

    public final int dayValue;

    private Weekday(int dayValue) {
        this.dayValue = dayValue;
    }
}
toString() - 返回和name()相同的值,但toString()可以被覆写,用于自定义输出内容
代码:
public class Main {
    public static void main(String[] args) {
        Weekday day = Weekday.MON;
        if (day == Weekday.SAT || day == Weekday.SUN) {
            System.out.println("Today is " + day + ". Work at home!");
        } else {
            System.out.println("Today is " + day + ". Work at office!");
        }
    }
}

enum Weekday {
    MON("星期一"), TUE("星期二"), WED("星期三"), THU("星期四"), FRI("星期五"), SAT("星期六"), SUN("星期日");

    private final String chinese;

    private Weekday(String chinese) {
        this.chinese = chinese;
    }

    @Override
    public String toString() {
        return this.chinese;
    }
}
注意:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!

switch
枚举类比int, String更适合于switch,因为枚举类具有有限个枚举常量,同时具有类型信息
default语句,可以在漏泄某个枚举常量时自动把偶从,及时发现错误
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-记录类

record类也叫数据类(data class),它是一种纯数据载体
record类通俗的说就是“字段,只是字段,除了字段什么也没有”的类

record
Java 14开始,引入了record类
代码:
public class Main {
    public static void main(String[] args) {
        Point p = new Point(123, 456);
        System.out.println(p.x());
        System.out.println(p.y());
        System.out.println(p);
    }
}

public record Point(int x, int y) {}
构造方法
给构造方法加上额外的逻辑,使用public Point {}
代码:
public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException();
        }
    }
}
这种无参数的构造方法被称为Compact Constructor,生成最终的构造方法是自编逻辑+原有赋值逻辑

使用静态方法of()创建record类
代码:
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);
    }
}
这样创建point数据类的写法更简洁:
代码:
var z = Point.of();
var p = Point.of(123, 456);
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-BigInteger

Java中整型最大范围是CPU原生提供的64位的long型整数,使用CPU指令计算速度比较快
如果超出long型,可以使用软件模拟,java.math.BigInteger就是用来表示任意大小的整数
BigInteger内部用int[]数组来模拟一个非常大的整数
代码:
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 1234567890的5次方:2867971860299718107233761438093672048294900000
BigInteger的运算只能用内部方法运算(不能使用常规运算符+-*/)
代码:
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
将BigInteger转为基本类型,可以用以下方法:

  • 转换为byte:byteValue() 或 byteValueExact()
  • 转换为short:shortValue() 或 shortValueExact()
  • 转换为int:intValue() 或 intValueExact()
  • 转换为long:longValue() 或 longValueExact()
  • 转换为float:floatValue() 或 floatValueExact()
  • 转换为double:doubleValue() 或 doubleValueExact()

转换时如果超出了基本类型的范围,前者会丢失高位信息,后者会抛出ArithmeticException异常
代码:
BigInteger i = new BigInteger("123456789000");
System.out.println(i.longValue()); // 123456789000
System.out.println(i.multiply(i).longValueExact()); // java.lang.ArithmeticException: BigInteger out of long range
如果超出了float的最大范围,会返回Infinity
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-BigDecimal

BigDecimal表示任意大小且精度完全准确的浮点数
代码:
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489
scale() - 返回小数位数(如果为负数,表示是整数,末尾有几个0)
stripTrailingZeros() - 格式化为一个相当但去掉末尾0的BigDecimal
代码:
BigDecimal d1 = new BigDecimal("123.45");
BigDecimal d2 = new BigDecimal("123.4500");
BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d3.stripTrailingZeros();
System.out.println(d1.scale()); // 2,两位小数
System.out.println(d2.scale()); // 4
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2,整数1234500,末尾有2个0
setScale() - 设置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
对BigDecimal做加、减、乘法,精度不会丢失
对BigDecimal做除法,可能会无法除尽,因此必须指定精度和如何截断
代码:
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,因为除不尽
调用divideAndRemainder()方法时,返回的数组包含两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数。
利用余数是否为0,可以轻松判断BigDecimal是否可以被整除

比较BigDecimal
比较必须使用compareTo()方法(因为使用equals()方法不但要比较值,还要求scale()相等)
代码:
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变为2
System.out.println(d1.compareTo(d2)); // 0
其实BigDecimal是通过BigInteger和scale来表示的(BigInteger表示整数位,scale表示小数位)
代码:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
    private final BigInteger intVal;
    private final int scale;
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Java核心类-常用工具类

Math
代码:
Math.abs(-100); //求绝对值
Math.max(100, 99); //取最大值
Math.min(1.2, 2.3); //取最小值
Math.pow(2, 10); //计算2的10次方
Math.sqrt(2); //计算2的平方根
Math.exp(2); //计算2的指数,e2
Math.log(4); //计算4的自然对数
Math.log10(100); //计算以10为底的100对数
Math.sin(3.14); //三角函数:正弦函数
Math.cos(3.14); //三角函数:余弦函数
Math.tan(3.14); //三角函数:正切函数
Math.asin(3.14); //三角函数:反正弦函数
Math.acos(3.14); //三角函数:反余弦函数
Math.PI; //常量:Π
Math.E; //常量:欧拉数
Math.random(); //0到1之间的随机数
Random类
Random类用来生成伪随机数(伪随机数:只要给一个初始种子,产生的随机序列是完全一样的)
使用nextInt(),nextLong(),nextFloat(),nextDouble()来创建对应类型的随机数
代码:
Random r = new Random(2021110); //给出种子,每次生成都一样;不给种子,系统会使用当前时间作为种子,每次都不一样
System.out.println(r.nextInt()); // -918569555,每次都一样
System.out.println(r.nextInt(10)); // 6,生成一个[0,10)之间的int
System.out.println(r.nextLong()); // 5251399265835125127,每次都一样
System.out.println(r.nextFloat()); // 0.630313,生成一个[0,1)之间的float
System.out.println(r.nextDouble()); // 0.14925300373855654,生成一个[0,1)之间的double
Math.random()实际也是调用了Random类,也是伪随机数,只是无法指定种子

SecureRandom类
SecureRandom类用来创建安全的随机数(使用RNG算法计算出,而实际上真随机数只能通过量子力学原理来获取)
代码:
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
无法指定种子,但可以指定高强度算法
代码:
SecureRandom 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));
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-Java的异常

调用方法调用某个函数时,如果调用失败,如何获得调用失败的信息?
方法1:返回约定的错误码(0表示正常,其他表示各种错误)
方法2:语言层面的异常处理机制

异常是一种class,带有类型信息。异常可以在任意地方抛出,只需要在上层进行捕获

异常类的根是Throwable,继承自Object
Throwable往下分为Error和Exception两个体系,前者表示严重错误(导致程序无法运行),后者表示运行时错误
Exception往下分为RuntimeExcption和非RuntimeExcption两类

Java规定:
必须捕获的异常:Exception类及其子类(除RuntimeExcption类及其子类以外)
不需捕获的异常:Error类及其子类 + RuntimeExcption类及其子类

注意:编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析

捕获异常
如果一个方法定义时使用throw Xxx来定义可能抛出的异常,那么调用该方法时就必须捕获异常
代码:
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
    ...
}

  • 对于必须捕获的异常,需要用try catch来环绕,catch捕获Exception及其子类,否则编译器会报错
  • 如果调用该方法时没有捕获异常,但在其上一层调用方法中进行捕获,也可以通过编译
  • 如果上一层调用方法也没有捕获异常,最终会需要在main方法中进行捕获
  • 如果main方法中也不想捕获异常,可以在main方法中声明throws Exception,这样内部就无需捕获了,代价是一旦出错程序将会中止
代码:
public static void main(String[] args) throws Exception {
    ...
}
打印异常栈
如果捕获后不需要进行处理,至少也需要打印异常栈
代码:
try {
    return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
    // 先记下来再说:
    e.printStackTrace();
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-捕获异常

try catch捕获异常时,把可能出现异常的语句放在try{...}中,然后用catch捕获对应的Exception及其子类

多catch语句
当出现一个try,多个catch时,会顺序向下匹配,匹配满足其中一个时,执行并终止匹配(也就是说,最终只会匹配一个catch)
由于时顺序匹配,所以相应的子类必须放在前面,以下是错误的:
代码:
try {
    process1();
    process2();
    process3();
} catch (IOException e) {
    System.out.println("IO error");
} catch (UnsupportedEncodingException e) { // 永远捕获不到
    System.out.println("Bad encoding");
}
finally语句
finally语句放在所有catch{...}最后,无论是否报错,都会执行(正确执行try->finally,错误执行try->catch->finally)
finally语句总会最后执行
代码:
try {
    process1();
    process2();
    process3();
} catch (UnsupportedEncodingException e) {
    System.out.println("Bad encoding");
} catch (IOException e) {
    System.out.println("IO error");
} finally {
    System.out.println("END");
}
捕获多种异常
如果catch中多种异常处理语句一致,可以使用以下写法:
代码:
catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
    System.out.println("Bad input");
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-抛出异常

异常的传播
如果一个异常抛出后,当前方法没有捕获,会抛到上层调用方法,直到遇到try catch捕获为止
在这个传播链中,调用printStackTrace()可以打印调用栈

抛出异常
分2步:

  • 创建某个Exception的实例
  • 用throw语句抛出
代码:
//普通写法
NullPointerException e = new NullPointerException();
throw e;
//简洁写法
throw new NullPointerException();
如果捕获异常后,在catch中又抛出一个新异常,为了能够使用printStackTrace()追踪,需要在catch中抛出新异常的时候,将原有异常的实例传参进去(这样新的Exception就拥有了原始Exception的信息)
代码:
catch (NullPointerException e) {
    throw new IllegalArgumentException(e);
}
在catch中抛出新异常,不会影响原有finally语句的执行。这时JVM会先执行finally,再抛出新异常

异常屏蔽
如果在finally中抛出异常,由于会先执行finally,然后抛出catch中的异常,但finally中已经有包含抛出异常,程序只能抛出一个异常,因此catch中的那个抛出就被屏蔽了
代码:
try {
    Integer.parseInt("abc");
} catch (Exception e) {
    System.out.println("catched");
    throw new RuntimeException(e); //被屏蔽 Suppressed Exception
} finally {
    System.out.println("finally");
    throw new IllegalArgumentException();
}
如果需要抛出时保留所有异常信息,需要对上面的代码改造
代码:
Exception origin = null;
try {
    Integer.parseInt("abc");
} catch (Exception e) {
    System.out.println("catched");
    origin = e; //保留原始异常
} finally {
    System.out.println("finally");
    Exception e = new IllegalArgumentException();
    if (origin != null){
        e.addSuppressed(origin); //加入原始异常信息 Suppressed Exception
    }
    throw e;
}
通过Throwable.getSuppressed()可以获取所有的Suppressed Exception。

一般情况下,不需要在finally中抛出异常,因此不太需要关心Suppressed Exception
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-常用异常与自定义异常

常用异常
引用:
Exception

├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException

├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException

├─ ParseException

├─ GeneralSecurityException

├─ SQLException

└─ TimeoutException
尽量使用常用异常
代码:
static void process1(int age) {
    if (age <= 0) {
        throw new IllegalArgumentException();
    }
}
自定义异常
大型项目可以自定义异常,但需要保持一个合理的异常继承体系
通常新建一个BaseException作为根异常,然后派生出各类业务的异常
BaseException通常从RuntimeException派生
代码:
public class BaseException extends RuntimeException {
}
其他业务类型的异常从BaseException派生
代码:
public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

...
自定义BaseException可以包含多个构造方法
代码:
public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-NullPointerException

NullPointerException是空指针异常(NPE),如果一个对象为null,那么调用它的方法或访问它的字段,就会产生NullPointerException
代码:
String s = null;
System.out.println(s.toLowerCase());
这个实际来说应该叫做Null Reference,空引用异常

处理NullPointerException
NullPointerException是代码逻辑错误,早发现早解决,严禁使用捕获来隐藏错误
为了避免这个错误,可以:
使用空字符串""代替null
代码:
private String name = "";
返回空字符串""而不是null
代码:
if (getFileSize(file) == 0) {
    // 返回空数组而不是null:
    return new String[0];
}
这样调用方无需检查是否为null。
如果调用方业务一定要根据null来判断逻辑,可以考虑用Optinal<T>
代码:
if (!fileExist(file)) {
    return Optional.empty();
}
调用方可以用Optional.isPresent()来判断是否有结果

定位NullPointerException
从Java 14开始,开启增强型NullPointerException详细信息有助于定位问题:
代码:
java -XX:+ShowCodeDetailsInExceptionMessages Main.java
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-使用断言

断言(Assertion)是一种调试程序的方式,一般在开发和测试中使用,因为一旦断言失败,会抛出AssertionError导致程序中止
代码:
double x = Math.abs(-123.45);
assert x >= 0;
System.out.println(x);
也可以为断言加上自定义消息,方便调试
代码:
assert x >= 0 : "x must >= 0";
JVM默认关闭断言指令,要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言
有选择的启用断言:
对特定类启用:-ea:com.ktsee.sample.Main
对特定包启动:-ea:com.ktsee.sample...(注意结尾有3个.)
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-使用JDK Logging

内置日志包java.util.logging可以用来输出日志,代替System.out.println()
代码:
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
JDK的Logging定义了7个级别,从严重到普通:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

内置的日志库缺点:

  • Logging在JVM启动时读取配置并初始化,main()开始执行时无法更改配置
  • 配置需要通过给JVM传递启动参数-Djava.util.logging.config.file=<config-file-name>

因此一般用其他日志系统
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-使用Commons Logging

Commons Logging是一个第三方日志库,它是由Apache创建的日志模块
它可以通过配置文件挂接不同的日志系统,比如默认挂接log4j,找不到会挂接JDK Logging
使用需要2步:

  • 从LogFactory获取Log类的实例
  • 使用Log类打印日志
代码:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Main {
    public static void main(String[] args) {
        Log log = LogFactory.getLog(Main.class);
        log.info("start...");
        log.warn("end.");
    }
}
由于Commons Logging是第三方包,需要下载并引入项目中

Commons Logging定义了个日志级别,默认是Info

  • FATAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG
  • TRACE


静态方法和实例方法中引用Log
代码:
//静态方法中引用Log
public class Main {
    static final Log log = LogFactory.getLog(Main.class);

    static void foo() {
        log.info("foo");
    }
}
代码:
//实例方法中引用Log,可以使用getClass()来获取当前实例化对象
public class Person {
    protected final Log log = LogFactory.getLog(getClass());

    void foo() {
        log.info("foo");
    }
}
另一个比较有用的重载方法,可以直接记录异常
代码:
try {
    ...
} catch (Exception e) {
    log.error("直接记录异常",e);
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

异常处理-使用Log4j

Log4j是一种日志记录实现系统,一般用来和日志接口Commons Logging配合使用


  • Log4j通过不同的Appender,把一条日志记录到不同的地方,例如console, file, socket, jdbc等
  • Log4j通过不同的Filter,过滤不同的日志,可以做到只记录部分日志
  • Log4j通过不同的Layout,格式化日志显示格式,例如自动添加日期,方法名等信息


Log4j通过log4j2.xml文件对上面的Appender,Filter,Layout进行配置,同时和Commons Logging一样,也需要从第三方库导入
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

发新话题