引言
兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来。所以奉劝大家,面向对象还是要掌握一门,虽然Python好写舒服,但是毕竟不能完全面向对象,也没有那么多的应用场景,所以,奉劝看到本文的各位,还是提前学好C#或者Java。
字符串和编码
String
- 在Java中,String是一个引用类型,它本身也是一个class。但是,Java编译器对String有特殊处理,即可以直接用"..."(这里的...是象征字符串的)来表示一个字符串
- Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]字段,以及没有任何修改char[]的方法实现的。
class Main { public static void main(String[] args) { String s =
; System.out.println(s); s = s.toUpperCase(); System.out.println(s); }}字符串比较
- 当我们想比较两个字符串时,是想比较两个字符串的内容是否相同。这个时候要用equals()而不能用==
class Main { public static void main(String[] args) { String s1 =
; String s2 =
; System.out.println(s1 == s2); System.out.println(s1.equals(s2)); }}
- 从表面上看,两个字符串用==和equals()比较都为true,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1和s2的引用就是相同的。
class Main { public static void main(String[] args) { String s1 =
; String s2 =
.toLowerCase(); System.out.println(s1 == s2); System.out.println(s1.equals(s2)); }}
- 两个字符串比较,必须总是使用equals()方法。
- 要忽略大小写比较,使用equalsIgnoreCase()方法。
- String类还提供了多种方法来搜索子串、提取子串。常用的方法有:
.contains(
);
- 注意到contains()方法的参数是CharSequence而不是String,因为CharSequence是String的父类。
- 搜索子串的更多的例子:
.indexOf(
);
.lastIndexOf(
);
.startsWith(
);
.endsWith(
);
- 提取子串的例子:
- 注意索引号是从0开始的。
.substring(2);
.substring(2, 4);
去除首尾空白字符
- 使用trim()方法可以移除字符串首尾空白字符。空白字符包括空格,\t,\r,\n:
.trim(); //
- 注意:trim()并没有改变字符串的内容,而是返回了一个新字符串。
- 另一个strip()方法也可以移除字符串首尾空白字符。它和trim()不同的是,类似中文的空格字符\u3000也会被移除:
.strip();
.stripLeading();
.stripTrailing();
- String还提供了isEmpty()和isBlank()来判断字符串是否为空和空白字符串:
.isEmpty();
.isEmpty();
.isBlank();
.isBlank();
替换子串
- 两种方法,一种是根据字符或者字符串替换。
String s =
;s.replace(
,
);
s.replace(
,
);
- 另一种是通过正则表达式替换:
String s =
;s.replaceAll(
,
);
分割字符串
- 要分割字符串,使用split()方法,并且传入的也是正则表达式:
String s =
;String[] ss = s.split(
);
拼接字符串
- 拼接字符串使用静态方法join(),它用指定的字符串连接字符串数组:
String[] arr = {
,
,
};String s = String.join(
, arr);
类型转换
- 要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法:
String.valueOf(123);
String.valueOf(45.67);
String.valueOf(
);
String.valueOf(
Object());
- 要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为int类型:
n1 = Integer.parseInt(
);
n2 = Integer.parseInt(
, 16);
- 把字符串转换为boolean类型:
b1 = Boolean.parseBoolean(
);
b2 = Boolean.parseBoolean(
);
- 要特别注意,Integer有个getInteger(String)方法,它不是将字符串转换为int,而是把该字符串对应的系统变量转换为Integer:
Integer.getInteger(
);
转换为char[]
- String和char[]类型可以互相转换,方法是:
char[] cs =
.toCharArray();
s =
(cs);
- 如果修改了char[]数组,String并不会改变:
- 这是因为通过new String(char[])创建新的String实例时,它并不会直接引用传入的char[]数组,而是会复制一份,所以,修改外部的char[]数组不会影响String实例内部的char[]数组,因为这是两个不同的数组。
class Main { public static void main(String[] args) {
[] cs =
.toCharArray(); String s =
String(cs); System.out.println(s); cs[0] =
; System.out.println(s); }}
从String的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
class Main { public static void main(String[] args) {
[] scores =
[] { 88, 77, 51, 66 }; Score s =
Score(scores); s.printScores(); scores[2] = 99; s.printScores(); }}class Score {
[] scores; public Score(int[] scores) {
.scores = Arrays.copyOf(scores, scores.length);
} 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编码。如果我们要手动把字符串转换成其他编码,可以这样做:
[] b1 =
.getBytes();
[] b2 =
.getBytes(
);
[] b2 =
.getBytes(
);
[] b3 =
.getBytes(StandardCharsets.UTF_8);
- 如果要把已知编码的byte[]转换为String,可以这样做:
[] b = ...String s1 =
String(b,
);
String s2 =
String(b, StandardCharsets.UTF_8);
- 始终牢记:Java的String和char在内存中总是以Unicode编码表示。
StringBuilder
- Java编译器对String做了特殊处理,使得我们可以直接用+拼接字符串。
String s =
;
(
i = 0; i < 1000; i++) { s = s +
+ i;}
- 虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
- 为了能高效拼接字符串,Java标准库提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时,不会创建新的临时对象:
StringBuilder sb =
StringBuilder(1024);
(
i = 0; i < 1000; i++) { sb.append(
); sb.append(i);}String s = sb.toString();
- StringBuilder还可以进行链式操作:
class Main { public static void main(String[] args) { var sb =
StringBuilder(1024); sb.append(
) .append(
) .append(
) .insert(0,
); System.out.println(sb.toString()); }}
- 如果我们查看StringBuilder的源码,可以发现,进行链式操作的关键是,定义的append()方法会返回this,这样,就可以不断调用自身的其他方法。使用链式操作的关键点就在于返回本身。
- 你可能还听说过StringBuffer,这是Java早期的一个StringBuilder的线程安全版本,StringBuilder和StringBuffer接口完全相同,现在完全没有必要使用StringBuffer。
StringJoiner
- 类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner来干这个事:
class Main { public static void main(String[] args) { String[] names = {
,
,
}; var sj =
StringJoiner(
);
(String name : names) { sj.add(name); } System.out.println(sj.toString()); }}
- 但是这样还不够,还少了开头的hello和结尾的!,于是我们给StringJoiner指定开头和结尾
class Main { public static void main(String[] args) { String[] names = {
,
,
};
var sj =
StringJoiner(
,
,
);
(String name : names) { sj.add(name); } System.out.println(sj.toString()); }}
- 其实StringJoiner的内部就是用的StringBuilder来拼接字符串的,所以拼接效率几乎和StringBuilder一模一样
String.join()
String[] names = {
,
,
};var s = String.join(
, names);包装类型
- Java的数据类型分两种:
- 基本类型:byte,short,int,long,boolean,float,double,char
- 引用类型:所有class和interface类型
- 引用类型可以赋值为null,表示空,但基本类型不能赋值为null:
String s =
;
n =
;
- 提问:如何把一个基本类型视为对象(引用类型)?
- 想要把int基本类型变成一个引用类型,我们可以定义一个Integer类,它只包含一个实例字段int,这样,Integer类就可以视为int的包装类(Wrapper Class):
- 想要把int基本类型变成一个引用类型,我们可以定义一个Integer类,它只包含一个实例字段int,这样,Integer类就可以视为int的包装类(Wrapper Class):
Integer n =
;Integer n2 =
Integer(99);
n3 = n2.intValue();
- 实际上因为包装类型非常有用,所以Java对于每一种基本类型都有他的包装类型。可以直接用,不用自行定义。
基本类型对应的引用类型
booleanjava.lang.Boolean
bytejava.lang.Byte
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double
charjava.lang.Character
class Main { public static void main(String[] args) {
i = 100;
Integer n1 =
Integer(i);
Integer n2 = Integer.valueOf(i);
Integer n3 = Integer.valueOf(
);
System.out.println(n3.intValue()); }}Auto Boxing
- 因为int和Integer可以互换,所以Java可以帮助我们在int和Integer之间转型
Integer n = 100;
x = n;
- 直接把int变为Integer的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。
自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
- 装箱和拆箱会影响代码的执行效率,因为编译后的class代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException:
不变类
- 所有的包装类型都是不变类。我们查看Integer的源码可知,它的核心代码如下:
class Integer {
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);
- 方法1:Integer n = new Integer(100);
- 方法2更好,因为方法1总是创建新的Integer实例,方法2把内部优化留给Integer的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
- 我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
进制转换
- Integer类本身还提供了大量方法,例如,最常用的静态方法parseInt()可以把字符串解析成一个整数:
x1 = Integer.parseInt(
);
x2 = Integer.parseInt(
, 16);
- Integer还可以把整数格式化为指定进制的字符串:
class Main { public static void main(String[] args) { System.out.println(Integer.toString(100));
System.out.println(Integer.toString(100, 36));
System.out.println(Integer.toHexString(100));
System.out.println(Integer.toOctalString(100));
System.out.println(Integer.toBinaryString(100));
}}
- 整数和浮点数的包装类型都继承自Number。
JavaBean
- 在Java中,有很多class的定义都符合这样的规范:
- 若干private实例字段;
- 通过public方法(getter、setter方法)来读写实例字段。
- 若干private实例字段;
class Person {
String name;
age; public String getName() {
.name; } public void setName(String name) {
.name = name; } public int getAge() {
.age; } public void setAge(int age) {
.age = age; }}
- 如果读写方法符合以下这种命名规范,则称为JavaBean
getXyz()
void setXyz(
value)
- boolean字段比较特殊,它的读方法一般命名为isXyz():
public boolean isChild()// 写方法:public void setChild(boolean value)
- 我们通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。例如,name属性:
- 对应的读方法是String getName()
- 对应的写方法是setName(String)
- 对应的读方法是String getName()
- 只有getter的属性称为只读属性(read-only),例如,定义一个age只读属性:
- 对应的读方法是int getAge()
- 无对应的写方法setAge(int)
- 对应的读方法是int getAge()
- 类似的,只有setter的属性称为只写属性(write-only)。
- 很明显,只读属性很常见,只写属性不常见。
JavaBean的作用
- JavaBean主要用来传递数据。
- JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
- 通过IDE,可以快速生成getter和setter。例如,在Eclipse中,先输入以下代码,然后,点击右键,在弹出的菜单中选择“Source”,“Generate Getters and Setters”,在弹出的对话框中选中需要生成getter和setter方法的字段,点击确定即可由IDE自动完成所有方法代码。
class Person {
String name;
age;}枚举JavaBean属性
- 要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector.getBeanInfo(ClassName.class)
枚举类
- 在Java中,我们可以通过static final来定义常量。例如,我们希望定义周一到周日这7个常量,可以用7个不同的int表示
class Weekday {
SUN = 0;
MON = 1;
TUE = 2;
WED = 3;
THU = 4;
FRI = 5;
SAT = 6;}
- 无论是int常量还是String常量,使用这些常量来表示一组枚举值的时候,有一个严重的问题就是,编译器无法检查每个值的合理性。例如:
(weekday == 6 || weekday == 7) {
(tasks == Weekday.MON) {
}}
- 上述代码编译和运行均不会报错,但存在两个问题:
- 注意到Weekday定义的常量范围是0~6,并不包含7,编译器无法检查不在枚举中的int值;
- 定义的常量仍可与其他变量比较,但其用途并非是枚举星期值。
- 注意到Weekday定义的常量范围是0~6,并不包含7,编译器无法检查不在枚举中的int值;
enum
- 为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum来定义枚举类。
class Main { public static void main(String[] args) { Weekday day = Weekday.SUN;
(day == Weekday.SAT || day == Weekday.SUN) { System.out.println(
); }
{ System.out.println(
); } }}
Weekday { SUN, MON, TUE, WED, THU, FRI, SAT;}
- 枚举的好处
- 编译器会自动检查出类型错误。
- 不可能引用到非枚举的值,因为无法通过编译。
- 不同类型的枚举不能互相比较或者赋值,因为类型不符。例如,不能给一个Weekday枚举类型的变量赋值为Color枚举类型的值。
Weekday x = Weekday.SUN;
Weekday y = Color.RED;
enum的比较
enum类型
- 定义的enum类型总是继承自java.lang.Enum,且无法被继承;
- 只能定义出enum的实例,而无法通过new操作符创建enum的实例;
- 定义的每个实例都是引用类型的唯一实例;
- 可以将enum类型用于switch语句。
enum Color { RED, GREEN, BLUE;}
public
class Color extends Enum {
public static
=
(); public static
=
(); public static
=
();
() {}}
name()
s = Weekday.SUN.
();
ordinal()
n = Weekday.MON.ordinal();
class Main { public static void main(String[] args) { Weekday day = Weekday.SUN;
(day.dayValue == 6 || day.dayValue == 0) { System.out.println(
); }
{ System.out.println(
); } }}
Weekday { MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
dayValue; private Weekday(int dayValue) {
.dayValue = dayValue; }}
class Main { public static void main(String[] args) { Weekday day = Weekday.SUN;
(day.dayValue == 6 || day.dayValue == 0) { System.out.println(
+ day +
); }
{ System.out.println(
+ day +
); } }}
Weekday { MON(1,
), TUE(2,
), WED(3,
), THU(4,
), FRI(5,
), SAT(6,
), SUN(0,
);
dayValue;
String chinese; private Weekday(int dayValue, String chinese) {
.dayValue = dayValue;
.chinese = chinese; }
public String toString() {
.chinese; }}
注意:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!
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 =
BigInteger(
);BigInteger i2 =
BigInteger(
);BigInteger sum = i1.add(i2);
BigInteger mul = i1.multiply(i2);
System.out.println(i.multiply(i).longValueExact());
BigDecimal
- 和BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal bd =
BigDecimal(
);System.out.println(bd.multiply(bd));
- BigDecimal用scale()表示小数位数,例如:
BigDecimal d1 =
BigDecimal(
);BigDecimal d2 =
BigDecimal(
);BigDecimal d3 =
BigDecimal(
);System.out.println(d1.scale());
System.out.println(d2.scale());
System.out.println(d3.scale());
- 通过BigDecimal的stripTrailingZeros()方法,可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal:
BigDecimal d1 =
BigDecimal(
);BigDecimal d2 = d1.stripTrailingZeros();System.out.println(d1.scale());
System.out.println(d2.scale());
BigDecimal d3 =
BigDecimal(
);BigDecimal d4 = d3.stripTrailingZeros();System.out.println(d3.scale());
System.out.println(d4.scale());
- 如果一个BigDecimal的scale()返回负数,例如,-2,表示这个数是个整数,并且末尾有2个0。
- 可以对一个BigDecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
java.math.BigDecimal;
java.math.RoundingMode;
class Main { public static void main(String[] args) { BigDecimal d1 =
BigDecimal(
); BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP);
BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN);
System.out.println(d2); System.out.println(d3); }}
- 对BigDecimal做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
BigDecimal d1 =
BigDecimal(
);BigDecimal d2 =
BigDecimal(
);BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP);
BigDecimal d4 = d1.divide(d2);
- 还可以对BigDecimal做除法的同时求余数:
java.math.BigDecimal;
class Main { public static void main(String[] args) { BigDecimal n =
BigDecimal(
); BigDecimal m =
BigDecimal(
); BigDecimal[] dr = n.divideAndRemainder(m); System.out.println(dr[0]);
System.out.println(dr[1]);
}}
- 调用divideAndRemainder()方法时,返回的数组包含两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个BigDecimal是否是整数倍数:
BigDecimal n =
BigDecimal(
);BigDecimal m =
BigDecimal(
);BigDecimal[] dr = n.divideAndRemainder(m);
(dr[1].signum() == 0) {
}比较BigDecimal
- 在比较两个BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等:
BigDecimal d1 =
BigDecimal(
);BigDecimal d2 =
BigDecimal(
);System.out.println(d1.equals(d2));
System.out.println(d1.equals(d2.stripTrailingZeros()));
System.out.println(d1.compareTo(d2));
- 必须使用compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。
- 总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!
- 如果查看BigDecimal的源码,可以发现,实际上一个BigDecimal是通过一个BigInteger和一个scale来表示的,即BigInteger表示一个完整的整数,而scale表示小数位数:
class BigDecimal extends Number implements Comparable<BigDecimal> {
BigInteger intVal;
scale;}
- BigDecimal也是从Number继承的,也是不可变对象。
常用工具类
Math
.abs(-100);
.abs(-7.8);
.max(100, 99);
.min(1.2, 2.3);
.pow(2, 10);
.sqrt(2);
.exp(2);
.log(4);
.log10(100);
.sin(3.14);
.cos(3.14);
.tan(3.14);
.asin(1.0);
.acos(1.0);
double pi =
.PI;
double e =
.E;
.sin(
.PI / 6);
.random();
{ public static void main(String[] args) {
x = Math.random();
min = 10;
max = 50;
y = x * (max - min) + min;
n = (
) y;
System.
.println(y); System.
.println(n); }}
Random
Random r =
Random();r.nextInt();
r.nextInt(10);
r.nextLong();
r.nextFloat();
r.nextDouble();
java.util.Random;
class Main { public static void main(String[] args) { Random r =
Random(12345);
(
i = 0; i < 10; i++) { System.out.println(r.nextInt(100)); }
}}
SecureRandom
SecureRandom sr =
SecureRandom();System.out.
(sr.nextInt(100));
java.util.Arrays;
java.security.SecureRandom;
java.security.NoSuchAlgorithmException;
class Main { public static void main(String[] args) { SecureRandom sr =
;
{ sr = SecureRandom.getInstanceStrong();
}
(NoSuchAlgorithmException e) { sr =
SecureRandom();
}
[] buffer =
[16]; sr.nextBytes(buffer);
System.out.println(Arrays.toString(buffer)); }}
SecureRandom的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。