常用基础类

包装类

Java有8中基本数据类型,每种基本数据类型都有对应的包装类型。包装类内部有一个实例变量,保存对应的基本类型的值,这些类还有一些静态方法、静态变量和实例方法,以方便对数据进行操作。Java中,基本类型和对应的包装类型如下表。

基本类型 包装类型 基本类型 包装类型
byte Byte short Short
int Integer long Long
float Float double Double
char Character boolean Boolean

包装类很好记,除了IntegerCharacter,其他的类名称和基本类型基本一样,只是首字母大写。包装类有什么用呢?Java中很多代码只能操作对象,为了能操作基本类型,需要使用其对应的包装类。另外,包装类提供了很多有用的方法,可以方便的对数据进行操作。

基本用法

每一个包装类都可以于其对应的基本类型相互转换,方法也是类似的。

  1. boolean b = false;
  2. Boolean b1 = Boolean.valueOf(b);
  3. boolean b2 = b1.booleanValue();
  4. int i = 12345;
  5. Integer i1 = Integer.valueOf(i);
  6. int i2 = i1.intValue();
  7. double d = 0.1;
  8. Double d1 = Double.valueOf(d);
  9. double d2 = d1.doubleValue();
  10. char c = 'A';
  11. Character c1 = Character.valueOf(c);
  12. char c2 = c1.charValue();

包装类于基本数据类型的转换结构是类似的,每种包装类都有一个静态方法valueOf(),接收基本类型,返回对应的引用类型,也都有一个实例方法Value返回对应的基本类型。

将基本类型转换为包装类的过程称之为”装箱”,而将包装类型转换为基本类型的过程称之为”拆箱”。拆箱和装箱写起来都比较繁琐,Java5之后引入了自动装箱和拆箱的技术,可以直接将基本类型赋值给引用类型,也可以将引用类型赋值给基本类型,比如:

  1. Integer a = 100;
  2. int b = a;

自动装箱和拆箱是Java编译器提供的功能,背后,它会自动调用对应的valueOfValue方法,比如,上面的代码会被Java编译器替换为:

  1. Integer a = Integer.valueOf(100);
  2. int b = a.intValue();

每种包装类也都有构造方法,可以通过new创建,比如:

  1. Integer i = new Integer(100);
  2. Boolean b = new Boolean(false);
  3. Double d = new Double(123.45);
  4. Character c = new Character('马');

那到底是用静态方法valueOf呢?,还是使用new呢?一般建议使用valueOf方法。new每次都会创建一个新对象,而除了FloatDouble外的其他包装类,都会缓存包装类对象,减少需要创建对象的次数,节省空间,提升性能。实际上,从Java9开始,这些构造方法已经被标记为过时了,推荐使用静态的valueOf方法。

共同点

各个包装类有很多特点,比如都重写了Object中的一些方法,都实现了Comparable接口,都有一些于String相关的方法,大部分都定义了静态常量,都是不可变的。

重写Object方法

所有包装类都重写了Object类的方法:

  1. boolean equals(Object obj);
  2. int hashCode();
  3. String toString();

equals

equals用于判断当前对象和参数传入的对象是否相同,Object默认的equals是比较地址,对于两个变量,当两个变量都指向同一个对象的时候,返回true,这与比较运算符的结果是一样的。

equals反映的是对象间的逻辑相等关系,所以这个默认实现一般不合适,子类需要重写该实现。所有包装类都重写了该实现,实际比较用的是其包装的基本类型值,比如,对于Long类型,其equals方法代码如下:

  1. public boolean equals(Object obj){
  2. if(obj instanceOf Long){
  3. return value == (Long obj).longValue();
  4. }
  5. return false;
  6. }

对于Float,其代码实现如下:

  1. public boolean equals(Object obj){
  2. return (obj instanceOf Float) && (flotToIntBits(((Float)obj).value) == floatToIntBits(value));
  3. }

Float有个静态方法floatToIntBits(),将float的二进制看作int,需要注意的是,只有两个float的二进制表示相等才会返回true,我们知道浮点运算是不精确的,数学上的概念运算结果一样,但计算机运算结果可能不一样,比如:

  1. Float f1 = 0.01f;
  2. Float f2 = 0.1f * 0.1f;
  3. print(f1.equals(f2)); // false
  4. print(Float.floatToIntBits(f1)); // 1008981770
  5. print(Float.floatToIntBits(f2)); // 1008981771

也就是,两个浮点数不一样,将二进制看作整数也不一样,相差为1。

Double的equals方法于Float类似,有一个静态方法doubleToLongBits,将Double的二进制看作Long,然后再按Long比较。

hashCode

hashCode返回一个对象的哈希值。哈希值是一个整数,由对象中一般不可变的属性映射得来,用于快速对对象进行区分,分组等。一个对象的哈希值不能改变,相同对象的哈希值必须一样。可以有对象不同但哈希值相同的情况。

hashCode与equals联系密切,对于两个对象,如果equals方法返回true,则hashCode必须也一样。反之不要求,equals方法返回false时,hashCode可以一样,但应该尽量不一样。hashCode的默认实现一般是将对象内存地址转换为整数,子类如果重写了equals方法,也必须重写hashCode。是因为Java很多API依赖于这个行为,尤其是容器类。

包装类都重写hashcode,根据包装类的基本类型值计算hashCode,对于Byte、Short、Integer,Character,hashCode就是其内部值,代码为:

  1. public int hashCode(){
  2. return (int)value;
  3. }
  4. // Boolean
  5. public int hashCode(){
  6. return value ? 1231 : 1237;
  7. }

根据基本类型返回了两个不同的数,因为他们是质数,质数用于哈希时比较好,不容易冲突。

  1. // Long
  2. public int hashCode(){
  3. return (int)(value ^ (value >>> 32));
  4. }

是高32位与低32位进行位异或操作。

对于Float,hashCode代码为:

  1. // Float
  2. public int hashCode(){
  3. return floatToIntBits(value);
  4. }

与equals方法类似,将float的二进制表示看做int。

对于Double,hashCode代码为:

  1. // Double
  2. public int hashCode(){
  3. long bits = doubleToLongBits(value);
  4. return (int)(bits ^ (bits >>> 32));
  5. }

与equals方法类似,将double的二进制表示看做long,然后按long计算hashCode。

Comparable

每个包装类都实现了Comparable接口,Comparable接口定义如下:

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

接口只有一个CompareTO方法,当前对象与参数对象进行比较,在小于、等于、大于参数时,应返回-1,0,1。各个包装类的实现基本是根据基本类型值进行比较,对于Boolean类型,false小于true。对于Float和Double,存在和equals一样的问题,0.01和0.1*0.1相比的结果并不为0。

包装类和toString

除了toString外,包装类还有一些其他与String相关的方法。除了Character外,每个包装类都有一个静态的valueOf(String)方法,根据字符串返回包装对象,如:

  1. Boolean b = Boolean.valueOf("true");
  2. Float f = Float.valueOf("123.45f");

也都有一个静态的parse(String)方法,根据字符串表示返回基本类型值,如:

  1. boolean b = Boolean.parseBoolean("true");
  2. double d = Double.parseDouble("123.45");

都有一个静态的toString方法,根据基本类型值返回字符串表示,如:

  1. pritn(Boolean.toString(true));
  2. print(Double.toString(123.45));

对于整数类型,字符串除了表示默认的十进制外,还可以表示为其他进制,如二进制、八进制、十进制和十六进制,包装类有静态方法进行相互转换,比如:

  1. // 输出二进制
  2. print(Integer.toBinaryString(12345)); // 11000000111001
  3. // 输出十六进制
  4. print(Integer.toHexString(12345)); // 3039
  5. // 按十六进制解析
  6. print(Integer.parseInt("3038",16)); // 12345

常用常量

包装类除了定义了一些常用的静态方法和实例方法之外,还定义了一些静态变量。对于Boolean类型,有:

  1. public static final Boolean TRUE = new Boolean(true);
  2. public static final Boolean FALSE = new Boolean(false);

所有数值类型都定义了MAX_VALUEMIN_VALUE,表示能表示的最大值/最小值,比如,对Integer:

  1. public static final Integer MAX_VALUE = 0x80000000;
  2. public static final Integer MIN_VALUE = 0x7fffffff;

Float和Double还定义了一些特殊数值,比如正无穷、负无穷、非数值,如Double类:

  1. public static final double POSITIVE_INFINITY = 1.0 / 0.0; // 正无穷
  2. public static final double NEGATIVE_INFINITY = -1.0 / 0.0; // 负无穷
  3. public static final double NaN = 0.0d / 0.0; // 非数值

Number类

6中数值类型的包装类都有一个共同的父类Number。Number是个抽象类,它定义了如下方法:

  1. byte byteValue();
  2. short shortValue();
  3. int intValue();
  4. long longValue();
  5. float floatValue();
  6. double doubleValue();

通过这些方法,包装类实例可以返回任意的基本数据类型。

不可变性

包装类是不可变类。所谓不可变类是指实例一旦创建,就没有办法修改了。这是通过如下方式强制实现的:

  • 所有包装类都声明了final,不能被继承。
  • 内部基本类型值私有,且声明了final
  • 没有定义setter方法。

不可变使得程序更为简单安全,因为不用操心数据被意外改写的可能,可以安全地共享数据,尤其是在多线程的情况下。

Integer和二进制算法

位翻转

Integer有两个静态方法,可以按位进行翻转:

  1. public static int reverse(int i);
  2. public static byte reverseBytes(int i);

循环位移

valueOf实现

在创建包装类对象时,可以使用静态valueOf方法,也可以使用new,但建议使用valueOf方法。

  1. public static Integer valueOf(int i){
  2. assert IntegerCache.high >= 127;
  3. if(i >= IntegerCache.low && i <= IntegerCache.high){
  4. return IntegerCache.cache[i + (-IntegerCache.low)]
  5. }
  6. return new Integer(i);
  7. }

它使用了IntegerCache,这是个私有静态内部类,如下所示:

  1. private static class IntegerCache {
  2. static final int low = -128;
  3. static final int high;
  4. static final Integer cache[];
  5. static {
  6. int h = 127;
  7. // 读取配置
  8. String intgerCacheHighPropValue = sum.misc.VM.getSavedProperty("java.lang.IntegerCache.high");
  9. if(integerCacheHighPropValue != null){
  10. int i = parseInt(integerCacheHightPropValue);
  11. // 配置的值应该>=127才会生效
  12. i = Math.max(i,127);
  13. // 配置的值不能超过该Integer.MAX_VALUE-(-low)-1
  14. h = Math.min(i,Integer.MAX_VALUE-(-low)-1);
  15. }
  16. high = h;
  17. // 创建数组
  18. cache = new Integer[(high - low)+1];
  19. int j = low;
  20. for(int k = 0; k < cache.length; k++){
  21. cache[k] = new Integer(j++);
  22. }
  23. }
  24. private IntegerCa`che(){
  25. }
  26. }

IntegerCache表示Integer缓存,其中的cache变量是一个静态Integer数组,在静态初始化代码块中初始化,默认情况下,保存了-128-127共256个整数对应的Integer对象。

valueOf代码中,如果数值位于被缓存的范围,即默认-128-127,则直接从IntegerCache中获取已经预先创建好的对象引用,只要不在缓存范围内,才通过new创建对象。

通过共享对象,可以节省内存空间,由于Integer是不可变的,所以缓存对象可以被安全的共享。Boolean、Byte、Short、Long、Character都有类似的实现。这种共享常用对象的思路,是一种常用的设计思路,它有一个名字,叫亨元模式,英文叫Flyweight,即共享的轻量级元素。

Character

Character封装了很多Unicode字符级别的操作,是Java文本处理的基础,注意不是char级别,Unicode字符并不等同于char。

Unicode

Unicode给世界上每个字符都分配了一个编号,编号范围为0x000000~0x10FFFF。编号范围在0x0000-0xFFFF的字符为常用字符,称BMP(Basic Multilingual Plane)字符。编号范围在0x10000-0x10FFFF的字符叫做增补字符(supplementary character)。

Unicode主要规定了编号,但没有规定如果把编号映射为二进制。UTF-16是一种编码方式,或者叫映射方式,它将编号映射为两个或四个字节(变长字节),对于常用字符(BMP),它直接使用两个字节表示,对于增补字符,使用四个字节表示,前两个字节叫高代理项(high surrogate),范围为0xD800-0xDBFF,后两个字节叫低代理向(low surrogate)范围为0xDC00-0xDFFF。UTF-16定义了一个公式,可以将编号与4字节表示进行相互转换。

Java内部采用UTF-16编码,char表示一个字符,但只能表示常用字符(BMP),对于增补字符,需要使用两个char表示,一个表示高代理项,一个表示低代理项。

使用int可以表示任意一个Unicode字符,低21位表示Unicode编号,高11位设为0。整数编号在Unicode中一般称为代码点(code point),表示一个Unicode字符,与之相对,还有一个词代码单元(code unit)表示一个char。

检查code point和code unit
  1. // 检查一个int是不是一个有效的代码点,小于等于0x10FFFF的为有效,大于的为无效
  2. public static boolean isValidCodePoint(int codePoint);
  3. // 判断一个int是不是BMP字符,小于等于0xFFFF的为BMP字符,大于的不是
  4. public static boolean isBmpCodePoint(int codePoint);
  5. // 判断一个int是不是增补字符,0x10000-0x10FFFF为增补字符
  6. public static boolean isSupplementaryCodePoint(int codePoint);
  7. // 判断char是不是高代理项,0xD800-0xDBFF为高代理项
  8. public static boolean isHighSurrogate(char ch);
  9. // 判断char是不是低代理项,0xDC00-0xDFFF为低代理项
  10. public static boolean isLowSurrogate(char ch);
  11. // 判断char是否为代理项,char为低代理项或高代理项,则返回true
  12. public static boolean isSurrogate(char ch);
  13. // 判断两个字符 high low是否分别为高代理项和低代项
  14. public static boolean isSurrogatePail(char high, char low);
  15. // 判断一个代码点由几个char组成 增补字符返回2 BMP字符返回1
  16. public static int charCount(int codePoint);

code point和char的转换
  1. // 根据高代理项high和低代理项low生成代码点,这个转换有个公式,这个方法封装了这个公式
  2. public static int toCodePointer(char high, char low);
  3. // 根据代码点生成char数组,即UTF-16表示,如果code point为BMP字符,则返回的char数组长度为1
  4. // 如果为增补字符,长度为2,char[0] 为高代理项 char[1]为低代理项
  5. public static char[] toChars(int codePoint);
  6. // 将代码点转换为char数组,与上面方法类似,只是结果存入指定数组dst的指定位置dstIndex
  7. public static int toChar(int codePoint,char[] dst, int dstIndex);
  8. // 对增补字符code point,生成低代理项
  9. public static char lowSurrogate(int codePoint);
  10. // 对增补字符codePoint,生成高代理项
  11. public static char highSurrogate(int codePoint);

按codePointer处理char数组或序列

Character包含若干方法,以方便按照codePoint处理char数组或序列。

返回char数组a中从offset开始count个char包含的code point个数:

  1. public static int codePointerCount(char[] a, int offset, int count);

比如,如下代码输出为2,char个数为3,但code point为2。

  1. char[] chars = new char[3];
  2. chars[0] = '马';
  3. Character.toChars(0x1FFFF, chars, 1);
  4. print(Character.codePointCount(chs, 0, 3));

除了接收char数组,还有一个重载的方法接收字符序列CharSequence:

  1. public static int codePointCount(CharSequence seq, int beginIndex, int endIndex);

CharSequence是一个接口,它的定义如下:

  1. public interface CharSequence{
  2. int length();
  3. char charAt(int index);
  4. CharSequence subSequence(int start, int end);
  5. public String toString();
  6. }

它与一个char数组是类似的,有length方法,有charAt方法根据索引获取字符,String类就实现了该接口。

返回char数组或序列中指定索引位置的code point:

  1. public static int codePointAt(char[] a, int index);
  2. public static int codePointAt(char[] a, int index, int limit);
  3. public static int coedPointAt(CharSequence seq, int index);

如果指定索引位置为高代理项,下一个位置为低代理项,则返回两组组成的code point,检查下一位置时,下一位置要小于limit,没传limit时,默认为a.length。

返回char数组或序列中指定索引位置之前的code point:

  1. public static int codePointBefore(char[] a, int index);
  2. public static int codePointBefore(char[] a, int index, int start);
  3. public static int codePointBefore(CharSequence seq, int index);

codePointAt是往后找,codePointBefore是往前找,如果指定位置为低代理项,且前一个位置为高代理项,则返回两项组成的codePoint,检查前一个位置时,前一个位置要大于等于start,没传start,默认为0。

根据code point偏移数计算char索引:

  1. public static int offsetByCodePoints(char[] a, int start, int count, int index, int codePointOffset);
  2. public static int offsetByCodePoints(CharSequence seq, int index, int codePoint);

如果字符数组或序列中没有增补字符,返回值为index+codePointOffset,如果有增补字符,则会将codePointOffset看作code point偏移,转换字符偏移,start和count取字符数组的子数组。

比如,如下代码:

  1. char[] chs = new char[3];
  2. Character.toChars(0x1FFFF, chs, 1);
  3. print(Character.offsetByCodePoints(chs, 0, 3, 1, 1));

输出结果为3,index和codePointOffset都为1,但第二个字符为增补字符,一个codePoint偏移是两个char偏移,所以结果为3。

字符属性

Unicode在给每个字符分配一个编号以外,还分配了一些属性,Character类封装了对Unicode字符属性的检查和操作。

  1. // 获取字符类型
  2. public static int getType(int codePoint);
  3. public static int getType(char ch);

Unicode给每个字符分配了一个类型,这个类型是非常重要的,很多其他检查和操作都是基于这个类型。getType()方法参数可以是int类型的code point,也可以是char类型。char类型只能处理BMP字符,而int类型可以处理所有字符。Character类中还有很多方法都是既可以接受int,也可以接受char类型,后续之列出int类型的方法。返回值是int,表示类型,Character类中定义了很多静态常量表示这些类型,表格列出了一些字符、type值、以及Character类中的常量的名称。

字符 type值 常量名称 解释
‘A’ 1 UPPCASE_LETTER 大写字符
‘a’ 2 LOWERCASE_LETTER 小写字符
‘马’ 5 OTHER_LETTER 其他字符
‘1’ 9 DECIMAL_DIGIT_NUMBER 数值
‘’ 12 SPACE_SEPARATOR 空白字符
‘\n’ 15 CONTROL 控制字符
‘-‘ 20 DASH_PUNCTUATION 破折号?
‘{‘ 21 START_PUNCTUATION 花括号?
‘_’ 23 CONNECTOR_PUNCTUATION 下划线?
‘&’ 24 OTHER_PUNCTUATION 其他符号
‘<’ 25 MATH_SYMBOL 数学符号
‘$’ 26 CURRENCY_SYMBOL 货币符号

检查字符是否在Unicode中被定义:

  1. public static boolean isDefined(int codePoint);

每个被定义的字符,其getType()返回值都不为0,如果返回值为0,表示无定义。注意与isValidCodePoint()的区别,后者只要数字不大于0x10FFFF都返回true

检查字符是否为数字:

  1. public static boolean isDigit(int codePoint);

getType()返回值为DECIMAL_DIGIT_NUMBER的字符为数字。需要注意的是,不光是字符’0’、’1’…..、’9’是数字,中文全角字符的0-9也是数字。比如:

  1. char ch = '9'; // 中文全角字符
  2. print((int)ch+","+Character.isDigit(ch)); // 65305,true

全角字符的9,Unicode编号为65305,它也是数字。

检查是否字母(Letter):

  1. public static boolean isLetter(int codePoint);

如果getType()的返回值为下列其中之一,则为Letter:

  • UPPERCASE_LETTER
  • LOWERCASE_LETTER
  • TITLECASE_LETTER
  • MODIFIER_LETTER
  • OTEHR_LETTER

除了TITLECASE_LETTEMODIFIER_LETTER,其他上表都有示例,而这两个平时碰到的比较少,就不介绍了。

检查是否为字母或数字:

  1. public static boolean isLetterOrDigit(int codePoint);

只要其中之一返回true就返回true。

检查是否为字母(Alphabetic):

  1. public static boolean isAlphabetic(int codePoint);

这也是检查是否为字母,与isLetter的区别是:isLetter返回true,isPlphabetic也必然返回true;此外,getType()值为LETTER_NUMBER时候,isPlahabetic也返回true,而isLetter返回false。LETTER_NUMBER中常见的字符有罗马数字字符,如:’I’,’II’。

检查是否为空格字符:

  1. public static void isSpaceChar(int codePoint);

getType()值为SPACE_SEPARATORLINE_SEPARATORPARAGRAPH_SEPARATOR时,返回true。这个方法其实不常用,因为它只能严格匹配空格字符本身,不能匹配实际产生空格效果的字符,如Tab控制键’\t’。

更常用的检查空格方法:

  1. public static boolean isWhitespace(int codePoint);

‘\t’、’\n’、’全角空格’和’半空格’的返回值都为true。

检查是否为小写字符:

  1. public static boolean isLowerCase(int codePoint);

常见的小写字符主要是小写英文字母a-z。

检查是否为大写字符:

  1. public static boolean isUpperCase(int codePoint);

常见的大写字符主要是大写英文字母A-Z。

检查是否为表意象形文字:

  1. public static boolean isIdeographic(int codePoint);

大部分中文都返回true。

检查是否为ISO-8859-1编码中的控制字符:

  1. public static boolean isISOControl(int codePoint);

0-31,127-159表示控制字符。

检查是否可作为Java标识符的第一个字符:

  1. public static boolean isJavaIdentifierStart(int codePoint);

Java标识符是Java中的变量名、函数名、类名等,字母(Alphabetic)、美元符号($)、下划线(_)可作为Java标识符的第一个字符,但数字字符不可以。

检查是否可作为Java标识符的中间字符:

  1. public static boolean isJavaIdentifierPart(int codePoint);

相比isJavaIdentifierStart(),主要多了数字字符,Java标识符的中间字符可以包含数字。

检查是否为镜像(mirrowed)字符:

  1. public static boolean isMirrored(int codePoint);

常见的镜像字符有:()、{}、<>、[],都有对应的镜像字符。

字符转换

Unicode除了规定字符属性外,对有大小写对应的字符、还规定了其对应的大小写,对有数值含义的字符,也规定了其数值。

Character有两个静态方法,对字符进行大小写转换:

  1. public static int toLowerCase(int codePoint);
  2. public static int toUpperCase(int codePoint);

这两个方法主要针对英文字符a-z和A-Z,例如:toLowerCase('A')返回’a’,toUpperCase('z')返回’Z’。

返回一个字符表示的数值:

  1. public static int getNumbericValue(int codePoint);

字符’0’-‘9’返回数值0-9,对于字符a-z,无论是小写字符还是大写字符,无论是普通英文还是中文全角,数值结果都是10-35。例如,如下代码的输出结果是一样的,都是10。

  1. print(Character.getNumbericValue('A')); // 全角大写A
  2. print(Character.getNumbericValue('A'));
  3. print(Character.getNumbericValue('a')); // 全角小写a
  4. print(Character.getNumbericValue('a'));

返回按给定进制表示的数值:

  1. public static int digit(int codePoint, int radix);

radix表示进制,常见的有二进制、八进制、十进制、十六进制,计算方式与get-numbericValue类似,只是会检查有效性,数值需要小于radix,如果无效,返回-1。例如:digit(‘F’,16)返回15,是有效的;但digit(‘G’,16)就无效,返回-1。

返回给定数值的字符形式:

  1. public static char forDigit(int digit, int radix);

digit(int codePoint, int radix)相比,进行相反转换,如果数字无效,返回’\0’。例如,Character.forDigit(15,16)返回’F’。

Integer类似,Character也有按字节翻转:

  1. public static char reverseBytes(char ch);

例如,翻转字符0x1234

  1. print(Integer.toHexString(Character.reverseBytes((char)0x1234))); // 3412

Character,它在Unicode字符级别(而非char级别)封装了字符的各种操作,通过将字符处理的细节交给Character类,其他类就可以在更高的层次上处理文本了。

String

基本用法
  1. // 可以通过常量定义String变量
  2. String name = "老马说编程";
  3. // 也可以通过new创建String变量
  4. String name = new String("老马说编程");
  5. // String可以直接使用+和+=运算符,如:
  6. String name = "老马";
  7. name += "说编程";
  8. String description = ",探索编程本质";
  9. print(name+description); // 老马说编程,探索编程本质

String类包括很多方法,以方便操作字符串,比如:

  1. // 判断字符串是否为空
  2. public boolean isEmepty();
  3. // 获取字符串长度
  4. public int length();
  5. // 取子字符串
  6. public String subString(int beginIndex);
  7. // 去子字符串 包含beginIndex 不包含endIndex
  8. public String subString(int beginIndex, int endInex);
  9. // 查找字符,返回第一个找到的索引,没找到返回-1
  10. public int indexOf(int ch);
  11. // 查找子串,返回第一个找到的索引,没找到返回-1
  12. public int indexOf(String str);
  13. // 从后面查找字符,返回第一个找到的所有,没找到返回-1
  14. public int lastIndexOf(int ch);
  15. // 从后面查找子串,返回第一个的所有,没找到返回-1
  16. public int lastIndexOf(String str);
  17. // 判断字符串中是否包含指定的字符序列
  18. public boolean contains(CharSequence s);
  19. // 判断字符串是否以给定的子字符串开头
  20. public boolean startsWith(String prefix);
  21. // 判断字符是否以给定的子字符串结尾
  22. public boolean endWith(String suffix);
  23. // 与其他字符串比较,看内容是否相同
  24. public boolean equals(Object anOjbect);
  25. // 忽略大小写比较内容是否相同
  26. public boolean equalsIgnoreCase(String anotherString);
  27. // 比较字符串大小
  28. public int compareTo(String anotherString);
  29. // 忽略大小写比较
  30. public int compareToIgnoreCase(String str);
  31. // 所有字符转换为大写字符,返回新字符串,原字符串不变
  32. public String toUpperCase();
  33. // 所有字符转换为小写字符,返回新字符串,原字符串不变
  34. public String toLowerCase();
  35. // 字符串拼接,返回当前字符串和参数字符串合并结果
  36. public String concat();
  37. // 字符串替换,替换单个字符
  38. public String replace(char oldChar, char newChar);
  39. // 字符串替换,替换字符序列,返回新字符串,原字符串不变
  40. public String replace(CharSequence target, CharSequence replacement);
  41. // 删除字符串开头和结尾空格,返回新字符串,原字符串不变
  42. public String trim();
  43. // 分割字符串,返回分割后的字符串数组
  44. public String[] split(String regex);

String内部

String内部用一个字符数组表示字符串,实例变量定义为:

  1. private final char value[];

String有两个构造方法,可以根据char数组创建String实例:

  1. public String(char value[]);
  2. public String(char value[], int offset, int count);

需要说明的是,String会根据参数新创建一个数组,并复制内容,而不会直接用参数中的字符数组。String中的大部分方法也都是操作这个字符数组。比如:

  1. length()方法返回的是这个数组的长度。
  2. subString()方法是根据参数,调用构造方法String(char value[],int offset, int count)新建了一个字符串。
  3. indexOf()方法查找字符串或者子字符串时是在这个数组中查找。

String还有一些方法,与这个char数组有关:

  1. // 返回指定索引位置的char
  2. public char charAt(int intdex);
  3. // 返回字符串对应的char数组,返回的是一个复制后的数组,而不是原数组
  4. public char[] toCharArray();
  5. // 将char数组中指定范围的字符复制到目标数组指定位置
  6. public void getChars(int srcBegin, int srcEnd, char dst[] int dstBegin);

与Character类似,String也提供了一些方法,按代码点对字符串进行处理。

  1. public int codePointAt(int index);
  2. public int codePointBefore(int index);
  3. public int codePointCount(int beginIndex, int endIndex);
  4. public int offsetByCodePoints(int index, int codePointOffset);

编码转换

String内部是按UTF-16BE处理字符的,对BMP字符,使用一个char,两个字节,对于增补字符,使用两个char,四个字节。不同编码可能用于不同的字符集,使用不同的字节数目,以及不同的二进制表示。如何处理这些编码?这些编码与Java内部表示之间如何相互转换呢?

Java内部使用Charset类表示各种编码,它有两个常用静态方法:

  1. // 返回系统默认编码
  2. public static Charset defaultCharset();
  3. // 返回给定编码名称的Charset对象
  4. public static Charset forName(String charsetName);
  5. print(Charset.defaultCharset()); // UTF-8
  6. Charset charset = Charset.forName("GB18030");

String类提供如下方法,返回指定字符串给定编码的字节表示:

  1. public byte[] getBytes();
  2. public byte[] getBytes(String charsetName);
  3. public byte[] getBytes(Charset charset);

第一个方法没有参数,使用系统默认编码;第二个方法参数为编码名称;第三个方法为Charset对象。

String类有如下构造方法,根据指定字节数组和编码返回解析后的字符串,也就是说根据给定的编码的字节表示,按给定编码解析字节数组:

  1. public String(byte bytes[], int offset, int length, String charsetName);
  2. public String(byte bytes[], Charset charset);

不可变性

与包装类类似,String类也是不可变类,即对象创建,就没办法修改了。String类也声明了final,不能被继承,内部char数组value也是final的,初始化后就不能变了。

String类看似提供了很多修改的方法,其实是通过创建新的String对象来实现的,原来的String对象不会被修改。比如,concat()方法的代码:

  1. public String concat(String str){
  2. int otherLen = str.length();
  3. if(otherLen == 0){
  4. return this;
  5. }
  6. int len = value.length;
  7. char buf[] = Arrays.copyOf(value, len + otherLen);
  8. str.getChars(buf, len);
  9. return new String(buf, true);
  10. }

通过Arrays.copyOf方法创建了一块新的字符数组,复制原内容,然后通过new创建了一个新的String对象,最后一行调用的是String的另一个构造方法,其定义为:

  1. String(char[] value, boolean share){
  2. this.value = value;
  3. }

这是一个非公开的构造方法,直接使用传递过来的数组作为内部数组。

与包装类类似,定义为不可变类,程序可以更加简单、安全、高效、容易理解。而如果频繁修改字符串,而每次修改都会新建一个字符串,那么性能太低,这时,可以考虑Java中的另两个类StringBuild和StringBuffer。

常量字符串

Java中的字符串常量是非常特殊的,除了可以直接复制给String变量外,它自己就像一个String的对象,可以直接调用String的实例方法。

  1. print("a".length());
  2. print("a".contains("a"));
  3. print("a".indexOf('a'));

实际上,这些常量就是String类型变量,在内存中,它们被放在一个共享的地方,这个地方称之为字符串常量池,它保存所有的常量字符串,每个常量只会保存一份,被所有使用者共享。当通过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应String类型的对象。

比如如下代码:

  1. String name1 = "老马说编程";
  2. String name2 = "老马说编程";
  3. print(name1 = name2);

输出为true。为什么呢?,可认为,”老马说编程”在常量池中有一个对应的String类型的对象,我们假定名称为laoma,上面的代码实际上类似于:

  1. String laoma = new String(new char[]{'老', '马', '说', '编', '程'});
  2. String name1 = laoma;
  3. String name2 = laoma;
  4. print(name1 == name2);

实际上只有String对象,三个变量都指向这个对象,name1==name2的结果就不言而喻了。

需要注意的是,如果不是通过常量直接赋值,而是通过new创建,==就不会返回true了,看下面代码:

  1. String name1 = new String("老马说编程");
  2. String name2 = new String("老马说编程");
  3. print(name1 == name2); // false

上面的代码类似于:

  1. String laoma = new Stirng(new char[]{'老', '马', '说', '编', '程'});
  2. String name1 = new String(laoma);
  3. String name2 = new String(laoma);
  4. print(name1 == name2);

String类中以String为参数的构造方法代码如下:

  1. public String(String original){
  2. this.value = original.value;
  3. this.hash = original.value;
  4. }

hash是String类中另一个实例变量,表示缓存的hashCode值。

可以看出,name1和name2指向两个不同的String对象,只是这两个对象内部的value值指向相同的数组。

hashCode

Stirng的hashCode定义如下:

  1. private int hash; // default to 0

hash变量缓存了hashCode方法的值,也就是说,第一次调用hashCode方法的时候,会把结果保存在hash这个变量中,以后再调用就直接返回保存的值。

String类的hashCode方法,如下:

  1. public int hashCode(){
  2. int h = hash;
  3. if(h == 0 && value.length > 0){
  4. char val[] = value;
  5. for(int i = 0; i < val.length; i++){
  6. h = 31 * h + val[i];
  7. }
  8. hash = h;
  9. }
  10. return h;
  11. }

如果缓存的hash不为0,就直接返回了,否则根据字符数组中的内容计算hash,计算方法是:s[0]*31^(n-1)+s[1]*31^(n-2)+s[n-1]

s表示字符串,s[0]表示第一个字符,n表示字符串长度,s[0]*31^(n-1)表示31的(n-1)次方再乘以第一个字符的值。

为什么要使用这个计算方法呢?使用这个公式,可以让hash值于每个字符的值有关,也与每个字符的位置相关,位置i(i >= 1)的因素通过31的(n-i)次方表示。使用31的大概有两个原因:一方面可以产生更分散的散列,即不同字符串hash值也一般不同;另一方面计算效率比较高,31乘h与32乘h-h即(h<<5)-h等价,可以用更高效的移位和减法操作代替乘法操作。

正则表达式

String类中,有一些方法接收的不是普通的字符串参数,而是正则表达式。正则表达式可以理解为一个字符串,但表达的是一个规则,一般用于文本匹配、查找、替换等。正则表达式具有丰富和强大的功能,是一个比较大的问题。

Java中有专门的类(Patter和Matcher)用于正则表达式,但对于简单情况,String类提供了更为简洁的操作,String类中接受正则表达是的方法有:

  1. // 分隔字符串
  2. public String[] split(String regex);
  3. // 检查是否匹配
  4. public boolean matches(String regex);
  5. // 字符串替换
  6. public String replaceFirst(String regex, String replacement);
  7. // 字符串匹配
  8. public String replaceAll(String regex, String replacement);

值得了解的是Java9对String实现进行了优化,它的内部不是char数组,而是byte数组,如果所有字符都是ASCII码,它可以使用一个字节表示一个字符,而不用UTF-16BE编码,节省内存。