到头来还是逃不开Java - Java13核心类
Java13核心类
没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删

引言
兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来。所以奉劝大家,面向对象还是要掌握一门,虽然Python好写舒服,但是毕竟不能完全面向对象,也没有那么多的应用场景,所以,奉劝看到本文的各位,还是提前学好C#或者Java。
字符串和编码
String
在Java中,
String是一个引用类型,它本身也是一个class。但是,Java编译器对String有特殊处理,即可以直接用"..."(这里的...是象征字符串的)来表示一个字符串Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的
private final char[]字段,以及没有任何修改char[]的方法实现的。
public class Main {
public static void main(String[] args) {
String s = "Hello";
System.out.println(s);
s = s.toUpperCase();
System.out.println(s);
}
}
字符串比较
- 当我们想比较两个字符串时,是想比较两个字符串的内容是否相同。这个时候要用
equals()而不能用==
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
- 从表面上看,两个字符串用
==和equals()比较都为true,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1和s2的引用就是相同的。
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
两个字符串比较,必须总是使用
equals()方法。要忽略大小写比较,使用
equalsIgnoreCase()方法。String类还提供了多种方法来搜索子串、提取子串。常用的方法有:
// 是否包含子串:
"Hello".contains("ll"); // true
注意到
contains()方法的参数是CharSequence而不是String,因为CharSequence是String的父类。搜索子串的更多的例子:
"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"
去除首尾空白字符
- 使用
trim()方法可以移除字符串首尾空白字符。空白字符包括空格,\t,\r,\n:
" \tHello\r\n ".trim(); // "Hello"
注意:
trim()并没有改变字符串的内容,而是返回了一个新字符串。另一个
strip()方法也可以移除字符串首尾空白字符。它和trim()不同的是,类似中文的空格字符\u3000也会被移除:
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
String还提供了isEmpty()和isBlank()来判断字符串是否为空和空白字符串:
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串
- 两种方法,一种是根据字符或者字符串替换。
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
- 另一种是通过正则表达式替换:
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"
类型转换
- 要把任意基本类型或引用类型转换为字符串,可以使用静态方法
valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
- 要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为
int类型:
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
- 把字符串转换为
boolean类型:
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
- 要特别注意,
Integer有个getInteger(String)方法,它不是将字符串转换为int,而是把该字符串对应的系统变量转换为Integer:
Integer.getInteger("java.version"); // 版本号,11
转换为char[]
String和char[]类型可以互相转换,方法是:
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
- 如果修改了
char[]数组,String并不会改变: - 这是因为通过
new String(char[])创建新的String实例时,它并不会直接引用传入的char[]数组,而是会复制一份,所以,修改外部的char[]数组不会影响String实例内部的char[]数组,因为这是两个不同的数组。
public class Main {
public static void main(String[] args) {
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s);
cs[0] = 'X';
System.out.println(s);
}
}
从
String的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
public class Main {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores();
scores[2] = 99;
s.printScores();
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
// 这样传入的就是scores的复制
this.scores = Arrays.copyOf(scores, scores.length);
// 使用如下方法也可以
// this.scores = scores.clone();
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}
字符编码
ASCII编码范围从0到127,每个字符用一个byte表示。GB2312使用两个byte表示一个中文字符。Unicode是全球统一编码,其中的UTF-8是变长编码,英文字符为1个byte,中文字符为3个byte。在Java中,
char类型实际上就是两个字节的Unicode编码。如果我们要手动把字符串转换成其他编码,可以这样做:
byte[] b1 = "Hello".getBytes(); // 按ISO8859-1编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
- 如果要把已知编码的
byte[]转换为String,可以这样做:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
- 始终牢记:Java的
String和char在内存中总是以Unicode编码表示。
StringBuilder
- Java编译器对
String做了特殊处理,使得我们可以直接用+拼接字符串。
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
- 虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响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还可以进行链式操作:
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder(1024);
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString());
}
}
如果我们查看
StringBuilder的源码,可以发现,进行链式操作的关键是,定义的append()方法会返回this,这样,就可以不断调用自身的其他方法。使用链式操作的关键点就在于返回本身。你可能还听说过
StringBuffer,这是Java早期的一个StringBuilder的线程安全版本,StringBuilder和StringBuffer接口完全相同,现在完全没有必要使用StringBuffer。
StringJoiner
- 类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个
StringJoiner来干这个事:
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
- 但是这样还不够,还少了开头的
hello和结尾的!,于是我们给StringJoiner指定开头和结尾
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
// param1 是需要给数组之间插入的字符串 para2和3是指定了StringJoiner的开头和结尾
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
- 其实StringJoiner的内部就是用的StringBuilder来拼接字符串的,所以拼接效率几乎和StringBuilder一模一样
String.join()
String还提供了一个静态方法join(),这个方法在内部使用了StringJoiner来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()更方便:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类型
- Java的数据类型分两种:
- 基本类型:
byte,short,int,long,boolean,float,double,char - 引用类型:所有
class和interface类型
- 基本类型:
- 引用类型可以赋值为
null,表示空,但基本类型不能赋值为null:
String s = null;
int n = null; // compile error!
- 提问:如何把一个基本类型视为对象(引用类型)?
- 想要把
int基本类型变成一个引用类型,我们可以定义一个Integer类,它只包含一个实例字段int,这样,Integer类就可以视为int的包装类(Wrapper Class):
- 想要把
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
- 实际上因为包装类型非常有用,所以Java对于每一种基本类型都有他的包装类型。可以直接用,不用自行定义。
| 基本类型 | 对应的引用类型 |
|---|---|
| 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 |
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());
}
}
Auto Boxing
- 因为
int和Integer可以互换,所以Java可以帮助我们在int和Integer之间转型
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
- 直接把
int变为Integer的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。
自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
- 装箱和拆箱会影响代码的执行效率,因为编译后的
class代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException:
不变类
- 所有的包装类型都是不变类。我们查看
Integer的源码可知,它的核心代码如下:
public final class Integer {
private final int value;
}
因此,一旦创建了
Integer对象,该对象就是不变的。对两个
Integer实例进行比较要特别注意:绝对不能用==比较,因为Integer是引用类型,必须使用equals()比较。(引用类型必须用equals()比较)编译器把
Integer x = 127;自动变为Integer x = Integer.valueOf(127);,为了节省内存,Integer.valueOf()对于较小的数,始终返回相同的实例,因此,==比较“恰好”为true,但我们绝不能因为Java标准库的Integer内部有缓存优化就用==比较,必须用equals()方法比较两个Integer。
按照语义编程,而不是针对特定的底层实现去“优化”。
因为
Integer.valueOf()可能始终返回同一个Integer实例,因此,在我们自己创建Integer的时候,以下两种方法:方法1:
Integer n = new Integer(100);方法2:
Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的
Integer实例,方法2把内部优化留给Integer的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。我们把能创建“新”对象的静态方法称为静态工厂方法。
Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
进制转换
Integer类本身还提供了大量方法,例如,最常用的静态方法parseInt()可以把字符串解析成一个整数:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
Integer还可以把整数格式化为指定进制的字符串:
public class Main {
public static void main(String[] args) {
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。
JavaBean
在Java中,有很多
class的定义都符合这样的规范:若干
private实例字段;通过
public方法(getter、setter方法)来读写实例字段。
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; }
}
- 如果读写方法符合以下这种命名规范,则称为JavaBean
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
boolean字段比较特殊,它的读方法一般命名为isXyz():
// 读方法:
public boolean isChild()
// 写方法:
public void setChild(boolean value)
我们通常把一组对应的读方法(
getter)和写方法(setter)称为属性(property)。例如,name属性:对应的读方法是
String getName()对应的写方法是
setName(String)
只有
getter的属性称为只读属性(read-only),例如,定义一个age只读属性:对应的读方法是
int getAge()无对应的写方法
setAge(int)
类似的,只有
setter的属性称为只写属性(write-only)。很明显,只读属性很常见,只写属性不常见。
JavaBean的作用
JavaBean主要用来传递数据。
JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
通过IDE,可以快速生成
getter和setter。例如,在Eclipse中,先输入以下代码,然后,点击右键,在弹出的菜单中选择“Source”,“Generate Getters and Setters”,在弹出的对话框中选中需要生成getter和setter方法的字段,点击确定即可由IDE自动完成所有方法代码。
public class Person {
private String name;
private int age;
}
枚举JavaBean属性
- 要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的
Introspector.getBeanInfo(ClassName.class)
枚举类
- 在Java中,我们可以通过
static final来定义常量。例如,我们希望定义周一到周日这7个常量,可以用7个不同的int表示
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;
}
- 无论是
int常量还是String常量,使用这些常量来表示一组枚举值的时候,有一个严重的问题就是,编译器无法检查每个值的合理性。例如:
if (weekday == 6 || weekday == 7) {
if (tasks == Weekday.MON) {
// TODO:
}
}
上述代码编译和运行均不会报错,但存在两个问题:
注意到
Weekday定义的常量范围是0~6,并不包含7,编译器无法检查不在枚举中的int值;定义的常量仍可与其他变量比较,但其用途并非是枚举星期值。
enum
- 为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用
enum来定义枚举类。
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
- 枚举的好处
- 编译器会自动检查出类型错误。
- 不可能引用到非枚举的值,因为无法通过编译。
- 不同类型的枚举不能互相比较或者赋值,因为类型不符。例如,不能给一个
Weekday枚举类型的变量赋值为Color枚举类型的值。
Weekday x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types
enum的比较
前面讲解过引用类型的比较需要使用equals(),虽然enum定义的是一种枚举类型,但是却可以例外用==来比较。这是因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==比较。
enum类型
通过enum定义的枚举类,和其他的class有什么区别?
答案是没有任何区别。enum定义的类型就是class,只不过它有以下几个特点:
- 定义的
enum类型总是继承自java.lang.Enum,且无法被继承; - 只能定义出
enum的实例,而无法通过new操作符创建enum的实例; - 定义的每个实例都是引用类型的唯一实例;
- 可以将
enum类型用于switch语句。
例如,我们定义的Color枚举类:
public enum Color {
RED, GREEN, BLUE;
}
编译器编译出的class大概就像这样:
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}
所以,编译后的enum类和普通class并没有任何区别。但是我们自己无法按定义普通class那样来定义enum,必须使用enum关键字,这是Java语法规定的。
因为enum是一个class,每个枚举的值都是class实例,因此,这些实例有一些方法:
name()
返回常量名,例如:
String s = Weekday.SUN.name(); // "SUN"
ordinal()
返回定义的常量的顺序,从0开始计数,例如:
int n = Weekday.MON.ordinal(); // 1
改变枚举常量定义的顺序就会导致ordinal()返回值发生变化。
如果不小心修改了枚举的顺序,编译器是无法检查出这种逻辑错误的。要编写健壮的代码,就不要依靠ordinal()的返回值。因为enum本身是class,所以我们可以定义private的构造方法,并且,给每个枚举常量添加字段:
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()可以被覆写,而name()则不行。我们可以给Weekday添加toString()方法。
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}
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;
}
}
注意:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!
switch
因为枚举类天生具有类型信息和有限个枚举常量,所以比int、String类型更适合用在switch语句中。
BigInteger
Java中提供的整形最大范围是个64位的
long,要是超过了这个范围就需要用BigInteger来表示数字。java.math.BigInteger就是用来表示任何数字的。BigInteger进行运算的时候只能用实例方法,而且和long整形运算比起来速度较慢。BigInteger和Integer、Long一样,也是不可变类,并且也继承自Number类。因为Number定义了转换为基本类型的几个方法:- 转换为
byte:byteValue() - 转换为
short:shortValue() - 转换为
int:intValue() - 转换为
long:longValue() - 转换为
float:floatValue() - 转换为
double:doubleValue()
- 转换为
通过上述方法,可以把
BigInteger转换成基本类型。如果BigInteger表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()、longValueExact()等方法(没有其他的typeValueExact方法),在转换时如果超出范围,将直接抛出ArithmeticException异常。
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
BigInteger mul = i1.multiply(i2); //不知道多大了
System.out.println(i.multiply(i).longValueExact());
// java.lang.ArithmeticException: BigInteger out of long range
// 使用longValueExact()方法时,如果超出了long型的范围,会抛出ArithmeticException
BigDecimal
- 和
BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489
BigDecimal用scale()表示小数位数,例如:
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的stripTrailingZeros()方法,可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal:
BigDecimal d1 = new BigDecimal("123.4500");
BigDecimal d2 = d1.stripTrailingZeros();
System.out.println(d1.scale()); // 4
System.out.println(d2.scale()); // 2,因为去掉了00
BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d3.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2
- 如果一个
BigDecimal的scale()返回负数,例如,-2,表示这个数是个整数,并且末尾有2个0。 - 可以对一个
BigDecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Main {
public static void main(String[] args) {
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做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
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做除法的同时求余数:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102
System.out.println(dr[1]); // 0.105
}
}
- 调用
divideAndRemainder()方法时,返回的数组包含两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个BigDecimal是否是整数倍数:
BigDecimal n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) {
// n是m的整数倍
}
比较BigDecimal
- 在比较两个
BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的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
- 必须使用
compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。 - 总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!
- 如果查看
BigDecimal的源码,可以发现,实际上一个BigDecimal是通过一个BigInteger和一个scale来表示的,即BigInteger表示一个完整的整数,而scale表示小数位数:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}
BigDecimal也是从Number继承的,也是不可变对象。
常用工具类
Math
顾名思义,Math类就是用来进行数学计算的,它提供了大量的静态方法来便于我们实现数学计算:
求绝对值:
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
生成一个随机数x,x的范围是0 <= x < 1:
Math.random(); // 0.53907... 每次都不一样
如果我们要生成一个区间在[MIN, MAX)的随机数,可以借助Math.random()实现,计算如下:
// 区间在[MIN, MAX)的随机数
public class Main {
public static void main(String[] args) {
double x = Math.random(); // x的范围是[0,1)
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的范围是[10,50)
long n = (long) y; // n的范围是[10,50)的整数
System.out.println(y);
System.out.println(n);
}
}
有些童鞋可能注意到Java标准库还提供了一个StrictMath,它提供了和Math几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath保证所有平台计算结果都是完全相同的,而Math会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math就足够了。
Random
Random用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
要生成一个随机数,可以使用nextInt()、nextLong()、nextFloat()、nextDouble():
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
有童鞋问,每次运行程序,生成的随机数都是不同的,没看出伪随机数的特性来。
这是因为我们创建Random实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
如果我们在创建Random实例时指定一个种子,就会得到完全确定的随机数序列:
import java.util.Random;
public class Main {
public static void main(String[] args) {
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
}
前面我们使用的Math.random()实际上内部调用了Random类,所以它也是伪随机数,只是我们无法指定种子。
SecureRandom
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom就是用来创建安全的随机数的:
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
SecureRandom无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
import java.util.Arrays;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public class Main {
public static void main(String[] args) {
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));
}
}
SecureRandom的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用SecureRandom来产生安全的随机数。
到头来还是逃不开Java - Java13核心类的更多相关文章
- 到头来还是逃不开Java - Java13面向对象基础
面向对象基础 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来.所以 ...
- 到头来还是逃不开Java - Java13程序基础
java程序基础 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来. ...
- Java程序设计19——类的加载和反射-Part-A
1 本文概要 本章介绍Java类的加载.连接和初始化的深入知识,并重点介绍Java反射相关的内容.本章知识偏底层点,这些运行原理有助于我们更好的把我java程序的运行.而且Java类加载器除了根加载器 ...
- 【Java并发核心三】CountDownLatch、CyclicBarrier及Phaser
个人感觉,看书学习还是需要“不求甚解”,因为一旦太过于计较小的得失,就容易钻牛角尖,学习进度也慢.我们完全可以先学一个大概,等到真正用到的时候再把那些细节丰富起来,就更有针对性. 所以,针对java并 ...
- JAVA的核心概念:接口(interface)
JAVA的核心概念:接口(interface) 接口与类属于同一层次,实际上,接口是一种特殊的抽象类. 如: interface IA{ } public interface: 公开接口 与 ...
- [java学习笔记]java语言核心----面向对象之this关键字
一.this关键字 体现:当成员变量和函数的局部变量重名时,可以使用this关键字来区别:在构造函数中调用其它构造函数 原理: 代表的是当前对象. this就是所在函数 ...
- [java学习笔记]java语言核心----面向对象之构造函数
1.构造函数概念 特点: 函数名与类名相同 不用定义返回值类型 没有具体的返回值 作用: 给对象进行初始化 注意: 默认构造函数 多个构造函数是以重载出现的 一个类中如果 ...
- 从字节码和JVM的角度解析Java核心类String的不可变特性
1. 前言 最近看到几个有趣的关于Java核心类String的问题. String类是如何实现其不可变的特性的,设计成不可变的好处在哪里. 为什么不推荐使用+号的方式去形成新的字符串,推荐使用Stri ...
- 沉淀再出发:如何在eclipse中查看java的核心代码
沉淀再出发:如何在eclipse中查看java的核心代码 一.前言 很多时候我们在eclipse中按F3键打算查看某一个系统类的定义的时候,总是弹出找不到类这样的界面,这里我们把核心对应的代码加进 ...
随机推荐
- Python 爬取必应壁纸
import re import os import requests from time import sleep headers = { "User-Agent": (&quo ...
- 《手把手教你构建自己的 Linux 系统》学习笔记(1)
第二章.基础准备--Linux 系统定制原理 Linux 系统的组成及架构 共享库和静态库 在 Linux 系统中,函数库包含两种形式:共享库和静态库. 共享库 共享库在程序运行时,为其提供所需要的函 ...
- 前后端分离-crud&svn
前后端分离-crud&svn 1. 跨域 1.1 什么是跨域 请求方与服务方的源不同,即跨域,包括: 1. 协议不同 2. 域名不同 3. 端口不同 1.2 跨域不一定存在跨域问题 什么情况下 ...
- 离散对数及其拓展 大步小步算法 BSGS
离散对数及其拓展 离散对数是在群Zp∗Z_{p}^{*}Zp∗而言的,其中ppp是素数.即在在群Zp∗Z_{p}^{*}Zp∗内,aaa是生成元,求关于xxx的方程ax=ba^x=bax=b的解, ...
- sbt package报错:a bytes-like object is required, not 'str'
Traceback (most recent call last): File , in <module> s.sendall(content) TypeError: a bytes-li ...
- 使用 VMware Workstation Pro 安装新的虚拟机
一.连接服务器 (1)“文件”右键 (2)输入用户名.密码连接服务器 二.创建新的虚拟机 (1) (2) (3) (4) (5) (6) (7) (8) 三.配置服务器 配置文档 - 链接:htt ...
- CGI、FastCGI和php-fpm的概念和区别和运行原理
CGI: CGI的英文是(COMMON GATEWAY INTERFACE)公共网关接口,它的作用就是帮助服务器与语言通信,这里就是nginx和php进行通信,因为nginx和php的语言不通,因此需 ...
- MVC开发之Razor的使用
一.运用布局和视图起始文件 布局的使用,可以简化视图,允许用户创建能够运用于多个视图的通用HTML,并使得维护更加容易.下面是一个简单的例子: 域模型类: public class Product { ...
- ZooKeeper启动报错:My id 3 not in the peer list
错误描述: 解决方法:查看zookeeper-3.4.2/conf目录下 编辑zoo.cfg文件 发现第三行有问题修改
- linux查找含有xxx内容的文件
find . -name *.json |xargs grep "6379" |awk '{print $1}'|uniq