首先来首歌曲来放松一下吧!
一、字符串和编码
1、字符串(String)
在Java中,String
是一个引用类型,它本身也是一个class
。但是,Java编译器对String
有特殊处理,即可以直接用"..."
来表示一个字符串!
实际上字符串在String
内部是通过一个char[]
数组表示的,因此,按下面的写法也是可以的!
因为String
太常用了,所以Java提供了"..."
这种字符串字面量表示方法。
Java字符串的一个重要特点就是字符串不可变 。这种不可变性是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的。
1 2 String s1 = "hello!" ; String s2 = new String(new char [] {'H' , 'e' , 'l' , 'l' , 'o' , '!' });
下面这个输出结果不一样,因为不可变性,所以其实就是指向变了!原来的字符串仍然在内存中!
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { String s = "Hello" ; System.out.println(s); s = s.toUpperCase(); System.out.println(s); } }
1.1 字符串比较(equal)
必须使用equals()
方法而不能用==
!
equal()
方法比较的是实实在在指向的内容!
==
则比较的指向的对象或实例是否相同!
一般情况下我们只需要比较内容,所以一定要射用equal方法!
从表面上看,两个字符串用==
和equals()
比较都为true
,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1
和s2
的引用就是相同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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)); String s3 = "hello" ; String s4 = "HELLO" .toLowerCase(); System.out.println(s3 == s4); System.out.println(s3.equals(s4)); } }
要忽略大小写比较,使用equalsIgnoreCase()
方法。
1 2 3 4 5 6 7 8 9 10 package com.test;public class codeTest { public static void main (String[] args) { String s = "hello" ; String t = s.toUpperCase(); System.out.println(s.equals(t)); System.out.println(s.equalsIgnoreCase(t)); } }
1.2 字符串搜索
contains()
:参数是CharSequence
而不是String
,因为CharSequence
是String
的父类。查找子串,返回bool型!
indexof()
:返回查找第一次出现子串的下标位置!
lastIndexOf()
:返回最后一次出现子串的下标位置!
startsWith()
:返回是否以子串开头的bool型值!
endwith()
:返回是否以子串结尾的bool型值!
substring()
:截取子串!
substring(a)
:返回下标a开始到最后!
substring(a, b)
:返回下标a开始到b为止,左闭右开!
1 2 3 4 5 6 7 8 9 10 11 "Hello" .contains("ll" ); "Hello" .indexOf("l" ); "Hello" .lastIndexOf("l" ); "Hello" .startsWith("He" ); "Hello" .endsWith("lo" ); "Hello" .substring(2 ); "Hello" .substring(2 , 4 ); "ll"
1.3 取出首尾空白字符
trim()
:移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
!并没有改变字符串的内容,而是返回了一个新字符串。
strip()
:移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符\u3000
也会被移除!
stripLeading()
:移除首部!
stripTrailing()
:移除尾部!
isEmpty()
:潘福安字符串是否为空!
isBlank()
:判断字符串是否为空白字符(空格)!
1 2 3 4 5 6 7 8 9 10 11 " \tHello\r\n " .trim(); "\u3000Hello\u3000" .strip(); " Hello " .stripLeading(); " Hello " .stripTrailing(); "" .isEmpty(); " " .isEmpty(); " \n" .isBlank(); " Hello " .isBlank();
1.4 子串替换(replace)
1 2 3 String s = "hello" ; s.replace('l' , 'w' ); s.replace("ll" , "~~" );
参考我之前在JavaScript教程的RegExp:点击这里!
当然语法不太相同,后面的java教程会讲到Re!
1 2 String s = "A,,B;C ,D" ; String tt = s.replaceAll("[,;\\s]+" , "," );
1.5 字符串分割
1 2 String s = "A,B,C,D" ; String[] ss = s.split("," );
1.6 字符串拼接
1 2 String[] arr = {"A" , "B" , "C" }; String s = String.join("***" , arr);
1.7 类型转换
其他类型转换为字符串:
valueof()
:把任意基本类型或引用类型转换为字符串,这是一个重载方法,编译器会根据参数自动选择合适的方法!
1 2 3 4 String.valueOf(123 ); String.valueOf(45.67 ); String.valueOf(true ); String.valueOf(new Object());
字符串转换为其他类型:
Integer.parseInt()
:int转换为String!
Boolean.parseBoolean()
:boolean转换为String!
1 2 3 4 5 int n1 = Integer.parseInt("123" ); int n2 = Integer.parseInt("ff" , 16 ); boolean b1 = Boolean.parseBoolean("true" ); boolean b2 = Boolean.parseBoolean("FALSE" );
Integer
的getInteger(String)
方法,它不是将字符串转换为int
,而是把该字符串对应的系统变量转换为Integer
:
1 System.out.println("java版本:" + Integer.getInteger("java.version" ));
1.8 String与char[]互转
String转char[]:使用toCharArray()方法!
char[]转String:使用new String()方法!
1 2 char [] cs = "Hello" .toCharArray(); String s = new String(cs);
通过new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组。
new String()
时:传入的是一个复制!
1 2 3 4 5 6 7 8 9 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); } }
当向类中传入引用时,外部改变会影响类的改变!
由于Score
内部直接引用了外部传入的int[]
数组,这会造成外部代码对int[]
数组的修改,影响到Score
类的字段。如果外部代码不可信,这就会造成安全隐患。
可以使用数组的clone()
方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.util.Arrays;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) { this .scores = scores; this .scores = scores.clone(); } public void printScores () { System.out.println(Arrays.toString(scores)); } }
2、字符编码
可参考廖雪峰的字符编码教程:点击这里!
始终牢记:Java的String
和char
在内存中总是以Unicode编码表示。
2.1 ASCII编码
在早期的计算机系统中,为了给字符编码,美国国家标准学会(American National Standard Institute:ANSI)制定了一套英文字母、数字和常用符号的编码,它占用一个字节,编码范围从0
到127
,最高位始终为0
,称为ASCII
编码。例如,字符'A'
的编码是0x41
,字符'1'
的编码是0x31
。
2.2 GB2312和GBK编码
如果要把汉字也纳入计算机编码,很显然一个字节是不够的。GB2312
标准使用两个字节表示一个汉字,其中第一个字节的最高位始终为1
,以便和ASCII
编码区分开。例如,汉字'中'
的GB2312
编码是0xd6d0
。
1、收录不同:GB2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;GBK共收入21886个汉字和图形符号。
2、表示不同:GB2312对任意一个图形字符都采用两个字节表示,并对所收汉字进行了“分区”处理,每区含有94个汉字/符号,分别对应第一字节和第二字节。GBK采用双字节表示,总体编码范围为8140-FEFE之间,首字节在81-FE之间,尾字节在40-FE之间。
3、处理功能不同:对于人名、古汉语等方面出现的罕用字,GB2312不能处理,这导致了后来GBK 及GB18030 汉字字符集的出现。
GBK: 汉字国标扩展码,基本上采用了原来GB2312-80所有的汉字及码位,并涵盖了原Unicode中所有的汉字20902,总共收录了883个符号, 21003个汉字及提供了1894个造字码位。 Microsoft简体版中文Windows 95 就是以GBK为内码,又由于GBK同时也涵盖了Unicode所有CJK汉字,所以也可以和Unicode做一一对应。
GB码,全称是GB2312-80《信息交换用汉字编码字符集 基本集》,1980年发布,是中文信息处理 的国家标准,在大陆及海外使用简体中文的地区(如新加坡等)是强制使用的唯一中文编码。P-Windows 3.2和苹果OS就是以GB2312为基本汉字编码, Windows 95 /98则以GBK为基本汉字编码、但兼容支持GB2312。GB码共收录6763个简体汉字、682个符号,其中汉字部分:一级字3755,以拼音排序,二级字3008,以偏旁排序。该标准的制定和应用为规范、推动中文信息化进程起了很大作用。
GBK编码是中国大陆制订的、等同于UCS的新的中文编码扩展国家标准。GBK工作小组于1995 年10月,同年12月完成GBK规范。该编码标准兼容GB2312,共收录汉字21003个、符号883个,并提供1894个造字码位,简、繁体字融于一库。
2.3 Unicode编码
为了统一全球所有语言的编码,全球统一码联盟发布了Unicode
编码,它把世界上主要语言都纳入同一个编码,这样,中文、日文、韩文和其他语言就不会冲突。
Unicode
编码需要两个或者更多字节表示!
2.4 UTF-8编码
因为英文字符的Unicode
编码高字节总是00
,包含大量英文的文本会浪费空间,所以,出现了UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码。通过UTF-8
编码,英文字符'A'
的UTF-8
编码变为0x41
,正好和ASCII
码一致,而中文'中'
的UTF-8
编码为3字节0xe4b8ad
。
UTF-8
编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8
编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。
在Java中,char
类型实际上就是两个字节的Unicode
编码。如果我们要手动把字符串转换成其他编码,可以这样做:
getBytes("UTF-8")
:这个编译通不过。。
使用getBytes(StandardCharsets.UTF_8)
才可以!
了解一下就好!
转换编码后,就不再是char
类型,而是byte
类型表示的数组。
1 2 3 4 byte [] b1 = "Hello" .getBytes(); byte [] b2 = "Hello" .getBytes("UTF-8" ); byte [] b2 = "Hello" .getBytes("GBK" ); byte [] b3 = "Hello" .getBytes(StandardCharsets.UTF_8);
将byte[]
转换为String
,可以这样做:
1 2 3 byte [] b = ... String s1 = new String(b, "GBK" ); String s2 = new String(b, StandardCharsets.UTF_8);
3、String存储方式
早期JDK版本的String
总是以char[]
存储:
1 2 3 4 5 public final class String { private final char [] value; private final int offset; private final int count; }
较新的JDK版本的String
则以byte[]
存储:
如果String
仅包含ASCII字符,则每个byte
存储一个字符,否则,每两个byte
存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String
通常仅包含ASCII字符:
对于使用者来说,String
内部的优化不影响任何已有代码,因为它的public
方法签名是不变的。
1 2 3 public final class String { private final byte [] value; private final byte coder;
二、StringBuilder
使用String
拼接字符串时,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC(垃圾回收)效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象:
其他许多方法去编译器就可以看到!
你可能还听说过StringBuffer
,这是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
StringBuilder
和StringBuffer
接口完全相同,现在完全没有必要使用StringBuffer
。
也就是说StringBuffer
已经是一个淘汰品了,不需要使用了!
最大作用:高效拼接字符串!
进行链式操作的关键是,定义的append()
方法会返回this
,这样,就可以不断调用自身的其他方法!
参数capacity为初始容量,不够时,自动扩大为当前的二倍!
也可以不写该参数!
1 2 3 4 5 6 7 8 9 10 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()); } }
来设计一个支持链式操作的类!关键当然是可以返回this即可!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Main { public static void main (String[] args) { Adder adder = new Adder(); adder.add(3 ) .add(5 ) .inc() .add(10 ); System.out.println(adder.value()); } }class Adder { private int sum = 0 ; public Adder add (int n) { sum += n; return this ; } public Adder inc () { sum ++; return this ; } public int value () { return sum; } }
对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
三、StringJoiner
最大作用:可以用分隔符拼接字符串,也可以指定开始和结束!
StringJoiner
内部实际上就是使用了StringBuilder
,所以拼接效率和StringBuilder
几乎是一模一样的!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.util.StringJoiner;public class Main { public static void main (String[] args) { String[] names = {"Bob" , "Alice" , "Grace" }; var sj = new StringJoiner(", " ); var sj = new StringJoiner(", " , "Hello " , "!" ); for (String name : names) { sj.add(name); } System.out.println(sj.toString()); } }
不需要开头结尾时,可以使用更方便的String.join()
1 2 String[] names = {"Bob" , "Alice" , "Grace" };var s = String.join(", " , names);
四、包装类型
Java的两种数据类型:
基本类型:byte
,short
,int
,long
,boolean
,float
,double
,char
引用类型:所有class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
:
1 2 String s = null ;int n = null ;
将基本类型转换为引用类型就是包装类型!
1、int转Integer
1 2 3 4 5 6 7 8 Integer n = null ; Integer n2 = new Integer(99 ); Integer n3 = 98 ; Integer n4 = Integer.valueOf(99 ); Integer n5 = Integer.valueOf("100" );int n6 = n2.intValue();int n7 = n3;
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
2、Auto Boxing
由于可以自动互转,所以可以直接简化的写,编译器自动完成转换加上相应语句!
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)。
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
:
1 2 3 4 5 6 Integer n = 100 ; int x = n; Integer n = null ;int i = n;
3、不变类(final class)
所有的包装类型都是不变类,如Integer的源码:
两个Integer
比较大小,不能使用==
, 一定要用equal()
方法!
,==
比较,较小的两个相同的Integer
返回true
,较大的两个相同的Integer
返回false
,这是因为Integer
是不变类,编译器把Integer x = 127;
自动变为Integer x = Integer.valueOf(127);
,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,因此,==
比较“恰好”为true
,但我们绝不能 因为Java标准库的Integer
内部有缓存优化就用==
比较,必须用equals()
方法比较两个Integer
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public final class Integer { private final int value; }public class Main { public static void main (String[] args) { Integer x = 127 ; Integer y = 127 ; Integer m = 99999 ; Integer n = 99999 ; System.out.println("x == y: " + (x==y)); System.out.println("m == n: " + (m==n)); System.out.println("x.equals(y): " + x.equals(y)); System.out.println("m.equals(n): " + m.equals(n)); } }
因为Integer.valueOf()
可能始终返回同一个Integer
实例,因此,在我们自己创建Integer
的时候,以下两种方法:
方法1:Integer n = new Integer(100);
方法2:Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的Integer
实例,方法2把内部优化留给Integer
的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
我们把能创建“新”对象的静态方法称为静态工厂方法 。Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
如果我们考察Byte.valueOf()
方法的源码,可以看到,标准库返回的Byte
实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例!
4、Integer的进制转换
使用Integer.parseInt()
方法或者是toString()....
等等!
输出结果都是String
类型!
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main (String[] args) { int x1 = Integer.parseInt("100" ); int x2 = Integer.parseInt("100" , 16 ); 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 )); } }
5、静态变量
一些静态变量:
1 2 3 4 5 6 7 8 9 Boolean t = Boolean.TRUE; Boolean f = Boolean.FALSE;int max = Integer.MAX_VALUE; int min = Integer.MIN_VALUE; int sizeOfLong = Long.SIZE; int bytesOfLong = Long.BYTES;
所有的整数和浮点数的包装类型都继承自Number
,因此,可以非常方便地直接通过包装类型获取各种基本类型:
1 2 3 4 5 6 7 8 Number num = new Integer(999 );byte b = num.byteValue();int n = num.intValue();long ln = num.longValue();float f = num.floatValue();double d = num.doubleValue();
5、无符号处理
在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte
、short
、int
和long
都是带符号整型,最高位是符号位。而C语言则提供了CPU支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。
因为byte
的-1
的二进制表示是11111111
,以无符号整型转换后的int
就是255
。
类似的,可以把一个short
按unsigned转换为int
,把一个int
按unsigned转换为long
。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main (String[] args) { byte x = -1 ; byte y = 1 ; short r = -1 ; int t = -1 ; System.out.println(Byte.toUnsignedInt(x)); System.out.println(Byte.toUnsignedInt(y)); System.out.println(Short.toUnsignedInt(r)); System.out.println(Integer.toUnsignedLong(t)); } }
无、JavaBean
在Java中,有很多class
的定义都符合这样的规范:
若干private
实例字段;
通过public
方法来读写实例字段。
1、JavaBean规范
如果读写方法符合以下这种命名规范:
1 2 3 4 5 6 7 8 9 10 public Type getXyz () public void setXyz (Type value) public boolean isChild () public void setChild (boolean value)
那么这种class
被称为JavaBean
:
上面的字段是xyz
,那么读写方法名分别以get
和set
开头,并且后接大写字母开头的字段名Xyz
,因此两个读写方法名分别是getXyz()
和setXyz()
我们通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)。例如,name
属性:
对应的读方法是String getName()
对应的写方法是setName(String)
只有getter
的属性称为只读属性(read-only),例如,定义一个age只读属性:
对应的读方法是int getAge()
无对应的写方法setAge(int)
类似的,只有setter
的属性称为只写属性(write-only)。
很明显,只读属性很常见,只写属性不常见。
getter和setter就实现了一种数据封装的方法!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 boolean isChild () { return age <= 6 ; } }
2、JavaBean作用
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
通过IDE,可以快速生成getter
和setter
!
3、枚举JavaBean属性
了解即可!
使用Introspector.getBeanInfo()
可以获取属性列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.test;import java.beans.BeanInfo;import java.beans.IntrospectionException;import java.beans.Introspector;import java.beans.PropertyDescriptor;public class codeTest { public static void main (String[] args) throws IntrospectionException { 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()); } } }class Persons { private String name; private int age; public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } }
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 age public int com.test.Person.getAge() public void com.test.Person.setAge(int) birth public int com.test.Person.getBirth() public void com.test.Person.setBirth(int) class public final native java.lang.Class java.lang.Object.getClass() null name public java.lang.String com.test.Person.getName() public void com.test.Person.setName(java.lang.String)
六、枚举类(enum)
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum
来定义枚举类!
注意到定义枚举类是通过关键字enum
实现的,我们只需依次列出枚举的常量名。
和int
定义的常量相比,使用enum
定义枚举有如下好处:
首先,enum
常量本身带有类型信息,即Weekday.SUN
类型是Weekday
,编译器会自动检查出类型错误。
其次,不可能引用到非枚举的值,因为无法通过编译。
最后,不同类型的枚举不能互相比较或者赋值,因为类型不符!
使用enum
定义的枚举类是一种引用类型。前面我们讲到,引用类型比较,要使用equals()
方法,如果使用==
比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用equals()
方法,但enum
类型可以例外。
因为enum
类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==
比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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!" ); } int day = 1 ; if (day == Weekday.SUN) { } } }enum Weekday { SUN, MON, TUE, WED, THU, FRI, SAT; }
1、enum类型
通过enum
定义的枚举类,和其他的class
有什么区别?
答案是没有任何区别。enum
定义的类型就是class
,只不过它有以下几个特点:
定义的enum
类型总是继承自java.lang.Enum
,且无法被继承;
只能定义出enum
的实例,而无法通过new
操作符创建enum
的实例;
定义的每个实例都是引用类型的唯一实例;
可以将enum
类型用于switch
语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 public enum Color { RED, GREEN, BLUE; }public final class Color extends Enum { public static final Color RED = new Color(); public static final Color GREEN = new Color(); public static final Color BLUE = new Color(); private Color () {} }
编译后的enum
类和普通class
并没有任何区别。但是我们自己无法按定义普通class
那样来定义enum
,必须使用enum
关键字,这是Java语法规定的。
因为enum
是一个class
,每个枚举的值都是class
实例!
2、常用方法
name()
:返回常量名!
ordinal()
:返回定义常量的顺序(从0开始)!
1 2 String s = Weekday.SUN.name(); int n = Weekday.MON.ordinal();
注意:当枚举类Weekday内部的顺序发生变化时,通过ordinal()
方法获取到的值也会相应改变!
想要不受影响,新增的变量值一定要放到最后!
解决方法:
要编写健壮的代码,就不要依靠ordinal()
的返回值。因为enum
本身是class
,所以我们可以定义private
的构造方法,并且,给每个枚举常量添加字段:
默认情况下,对枚举常量调用toString()
会返回和name()
一样的字符串。但是,toString()
可以被覆写,而name()
则不行。我们可以给Weekday
添加toString()
方法:
覆写toString()
的目的是在输出时更有可读性。
注意:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!
默认调用day的toString()方法!
toString()覆写后则调用覆写后的方法!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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; } }
3、使用switch语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Main { public static void main (String[] args) { Weekday day = Weekday.SUN; switch (day) { case MON: case TUE: case WED: case THU: case FRI: System.out.println("Today is " + day + ". Work at office!" ); break ; case SAT: case SUN: System.out.println("Today is " + day + ". Work at home!" ); break ; } } }enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN; }
七、记录类(record)
使用String
、Integer
等类型的时候,这些类型都是不变类,一个不变类具有以下特点:
定义class时使用final
,无法派生子类;
每个字段使用final
,保证创建实例后无法修改任何字段。
为了保证不变类的比较,还需要正确覆写equals()
和hashCode()
方法,这样才能在集合类中正常使用。
从Java 14开始,引入了新的Record
类。我们定义Record
类时,使用关键字record
,
1、record类
除了用final
修饰class以及每个字段外,编译器还自动为我们创建了构造方法,和字段名同名的方法,以及覆写toString()
、equals()
和hashCode()
方法。
换句话说,使用record
关键字,可以一行写出一个不变类。
和enum
类似,我们自己不能直接从Record
派生,只能通过record
关键字由编译器实现继承。
1 2 3 4 5 6 7 8 9 10 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) {}
编译时编译器自动完成所需要的代码以及需要覆写的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public final class Point extends Record { private final int x; private final int y; public Point (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("Point[x=%s, y=%s]" , x, y); } public boolean equals (Object o) { ... } public int hashCode () { ... } }
2、record的构造方法
编译器默认按照record
声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。那么问题来了,如果我们要检查参数,应该怎么办?
假设Point
类的x
、y
不允许负数,我们就得给Point
的构造方法加上检查逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 IllegalArgumentException(); } this .x = x; this .y = y; } ... }
3、可以编写静态方法
1 2 3 4 5 6 7 8 9 10 11 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); } }var z = Point.of();var p = Point.of(123 , 456 );
4、小结:
从Java 14开始,提供新的record
关键字,可以非常方便地定义Data Class:
使用record
定义的是不变类;
可以编写Compact Constructor(构造方法)对参数进行验证;
可以定义静态方法。
八、BigInteger
超出long的范围时可以使用java.math.BigInteger
,来模拟大整数!
BigInteger
内部用一个int[]
数组来模拟一个非常大的整数!
做运算时,只能通过实例方法来进行!
和long
型整数运算比,BigInteger
不会有范围限制,但缺点是速度比较慢!
可以使用longVanlue()
方法来将其转化为long类型,前提当然是没有超过long的范围!超过会把报错!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 BigInteger bi = new BigInteger("1234567890" ); System.out.println(bi.pow(5 )); BigInteger i1 = new BigInteger("1234567890" ); BigInteger i2 = new BigInteger("12345678901234567890" ); BigInteger sum = i1.add(i2); BigInteger i = new BigInteger("123456789000" ); System.out.println(i.longValue()); System.out.println(i.multiply(i).longValueExact()); BigInteger s1 = s.add(new BigInteger("328423" )); BigInteger s3 = s.subtract(new BigInteger("89789797" )); BigInteger s2 = s.multiply(new BigInteger("328423" )); BigInteger s4 = s.divide(new BigInteger("2379743489" )); BigInteger s5 = s.pow(5 );
BigInteger
和Integer
、Long
一样,也是不可变类,并且也继承自Number
类。因为Number
定义了转换为基本类型的几个方法:
转换为byte
:byteValue()
转换为short
:shortValue()
转换为int
:intValue()
转换为long
:longValue()
转换为float
:floatValue()
转换为double
:doubleValue()
因此,通过上述方法,可以把BigInteger
转换成基本类型。如果BigInteger
表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()
、longValueExact()
等方法,在转换时如果超出范围,将直接抛出ArithmeticException
异常。
float没有floatValueExact()
方法,超出范围会输出Infintity
;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.math.BigInteger;public class Main { public static void main (String[] args) { BigInteger n = new BigInteger("999999" ).pow(99 ); float f = n.floatValue(); System.out.println(f); int s = n.intValueExact(); System.out.println(s); int s = n.intValue(); System.out.println(s); } }
九、BigDecimal
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal
也是从Number
继承的,也是不可变对象。
当然也有和BigInteger一样的方法!
BigDecimal
用于表示精确的小数,常用于财务计算;
1 2 BigDecimal bd = new BigDecimal("123.4567" ); System.out.println(bd.multiply(bd));
scale()
:可以计算小数位数!
stripTrailingZeros()
:去掉小数末尾的0!
若为整数,则返回整数末尾的0的个数,为负值!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 BigDecimal d1 = new BigDecimal("123.45" ); BigDecimal d2 = new BigDecimal("123.4500" ); BigDecimal d3 = new BigDecimal("1234500" ); System.out.println(d1.scale()); System.out.println(d2.scale()); System.out.println(d3.scale()); BigDecimal d1 = new BigDecimal("123.4500" ); BigDecimal d2 = d1.stripTrailingZeros(); System.out.println(d1.scale()); System.out.println(d2.scale()); BigDecimal d3 = new BigDecimal("1234500" ); BigDecimal d4 = d3.stripTrailingZeros(); System.out.println(d3.scale()); System.out.println(d4.scale());
setScale()
:设置精度,小数位数。两种截断方法:
RoundingMode.HALF_UP
:四舍五入截断
RoundingMode.DOWN
:直接截断
1 2 3 4 5 6 7 8 9 10 11 12 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); BigDecimal d3 = d1.setScale(4 , RoundingMode.DOWN); System.out.println(d2); System.out.println(d3); } }
对BigDecimal
做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
1 2 3 4 BigDecimal d1 = new BigDecimal("123.456" ); BigDecimal d2 = new BigDecimal("23.456789" ); BigDecimal d3 = d1.divide(d2, 10 , RoundingMode.HALF_UP); BigDecimal d4 = d1.divide(d2);
divideAndRemainder()
:返回除数和余数!
signum()
:有余数返回1,余数为0返回0!
调用divideAndRemainder()
方法时,返回的数组包含两个BigDecimal
,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个BigDecimal
是否是整数倍数:
1 2 3 4 5 6 7 8 9 10 11 12 BigDecimal n = new BigDecimal("12.345" ); BigDecimal m = new BigDecimal("0.12" ); BigDecimal[] dr = n.divideAndRemainder(m); System.out.println(dr[0 ]); System.out.println(dr[1 ]); BigDecimal n = new BigDecimal("12.75" ); BigDecimal m = new BigDecimal("0.15" ); BigDecimal[] dr = n.divideAndRemainder(m);if (dr[1 ].signum() == 0 ) { }
equal()
方法,不但要求两个BigDecimal
的值相等,还要求它们的scale()
相等!
可以使用stripTrailingZeros()
去掉末尾0再比较!
也可以使用compareTo()
方法:它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于。
比较必须使用compareTo()
方法!
1 2 3 4 5 BigDecimal d1 = new BigDecimal("123.456" ); BigDecimal d2 = new BigDecimal("123.45600" ); System.out.println(d1.equals(d2)); System.out.println(d1.equals(d2.stripTrailingZeros())); System.out.println(d1.compareTo(d2));
十、其他常用类
1、Math类
Java标准库还提供了一个StrictMath
,它提供了和Math
几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath
保证所有平台计算结果都是完全相同的,而Math
会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math
就足够了。
abs()
:绝对值
max()
:最大
min()
:最小
sqrt()
:开方
exp()
:e的x次方
log()
:以e为底对数
log10()
:以10为底对数
sin()、cos()、tan()、asin()、acos()
:三角函数
PI
:pai(3.14…)
E
:e(2.718…)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Math.abs(-100 ); Math.max(100 , 99 ); Math.min(1.2 , 2.3 ); Math.pow(2 , 10 ); Math.sqrt(2 ); Math.exp(2 ); Math.log(4 ); Math.log10(100 ); Math.sin(3.14 ); Math.cos(3.14 ); Math.tan(3.14 ); Math.asin(1.0 ); Math.acos(1.0 ); double pi = Math.PI; double e = Math.E; Math.sin(Math.PI / 6 );
2、Random类
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
要生成一个随机数,可以使用nextInt()
、nextLong()
、nextFloat()
、nextDouble()
:
1 2 3 4 5 6 Random r = new Random(); r.nextInt(); r.nextInt(10 ); r.nextLong(); r.nextFloat(); r.nextDouble();
倘若在创建实例时给定一个种子,则随机生成的数都是一定的,不会改变,不给种子是按照当前的时间戳自动确定种子,由于每时每刻时间不同,所以生成的数不同:
1 2 Random r = new Random(3 ); System.out.println(r.nextInt());
3、SecureRandom类
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的!
SecureRandom
无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom
实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
SecureRandom
的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用SecureRandom
来产生安全的随机数。
需要使用安全随机数的时候,必须使用SecureRandom,绝不能使用Random!
虽然有点没看懂!以后回来再看!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.test;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.util.Arrays;public class codeTest { public static void main (String[] args) { SecureRandom s = new SecureRandom(); System.out.println(s.nextInt()); SecureRandom sr = null ; try { sr = SecureRandom.getInstanceStrong(); } catch (NoSuchAlgorithmException e) { sr = new SecureRandom(); } byte [] buffer = new byte [16 ]; sr.nextBytes(buffer); System.out.println(Arrays.toString(buffer)); } }
常用Java核心类终于完结,敬请期待后续内容!