文章目录
- 一、分支循环
- 1.1 分支结构
- 1.2 循环结构
- 1.3 跳转语句
- 1.4 分支循环相关问题
- 1.4.1 switch是否能作用在byte上,是否能作用在long上,是否能作用在String上*
- 1.4.2 continue、break、和return的区别
- 1.4.3 switch(字符串)的实现原理
- 1.4.4 if和switch的性能对比
- 1.4.5 让if...else更优雅的几种写法
- 1.4.6 循环体中的语句要考量性能
- 二、数组
- 2.1 声明数组
- 2.2 初始化数组
- 2.3 遍历数组
- 2.4 数组作为方法参数/方法返回值
- 2.5 数组复制
- 2.6 多维数组
- 2.7 数组在内存中如何分配
- 三、字符串
- 3.1 String的创建
- 3.2 String的使用
- 3.2.1 获取指定位置字符
- 3.2.2 字符串比较
- 3.2.3 字符串连接
- 3.2.4 判断是否以指定子串开头/结尾
- 3.2.5 检索字符/子字符串的所在位置
- 3.2.6 获取字符串长度
- 3.2.7 分割字符串
- 3.2.8 截取字符串
- 3.2.9 删除字符串的头尾空白符
- 3.2.10 将不同类型的值转化为字符串
- 3.2.11 大小写转换
- 3.3 String优化
- 3.3.1 编译优化
- 3.3.2 intern优化
- 3.4 String相关问题
- 3.4.1 什么是字符串常量池?
- 3.4.2 String有哪些特性?
- 3.4.3 为什么String在Java中是不可变的*
- 3.4.4 常量池的使用
- 3.4.5 常规字符串拼接*
- 3.4.6 特殊字符串拼接
- 3.4.7 String s=new String("abc") 创建了几个对象*
- 3.4.8 equals与==的区别
- 3.4.9 String str="i"与 String str=new String(“i”)一样吗?
- 3.4.10 在使用HashMap的时候,用String做key有什么好处?
- 3.4.11 String编码,UTF-8和GBK的区别?
- 3.4.12 数组有没有length()方法?String有没有length()方法?
- 3.4.13 如何实现字符串的反转
- 3.4.14 怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?
- 3.4.15 String s="a"+"b"+"c"+"d";一共创建了多少个对象
- 3.4.16 String性能提升的几个小技巧
- 3.4.17 String类能被继承吗
- 3.5 可变字符串
- 3.5.1 StringBuffer
- 3.5.2 StringBuilder
- 3.5.3 String、StringBuffer和StringBuilder的比较*
- 3.5.4 StringBuilder在日志框架中的使用
- 3.5.5 字符串拼接原理
- 3.5.6 为什么StringBuilder.append()⽅法⽐"+="的性能⾼
- 四、方法
- 4.1 方法的定义
- 4.2 变量作用域
- 五、常用类
- 5.1 Math类
- 5.2 Arrays类
- 5.3 Collections类
- 5.4 Object类
- 5.4.1 Object中的重要方法
- 5.4.2 hashCode与equals*
- 5.4.3 hashCode与equals的区别*
- 5.4.4 hashCode与equals的相关问题
本系列文章: Java(一)数据类型、变量类型、修饰符、运算符 Java(二)分支循环、数组、字符串、方法 Java(三)面向对象、封装继承多态、重写和重载、枚举 Java(四)内部类、包装类、异常、日期 Java(五)反射、克隆、泛型、语法糖、元注解 Java(六)IO、NIO、四种引用 Java(七)JDK1.8新特性 Java(八)JDK1.17新特性
一、分支循环
Java中,除了普通的顺序结构外,特殊的结构有三种:分支结构、循环结构和跳转语句。
1.1 分支结构
if、if-else、if-else if-else结构适合某个变量值在连续区间
情况下的语句执行。
如果某个变量值不是在连续区间的话,比如是整数,可以用switch。switch基本语法:
switch(expression){
case value1 :
//语句
break; //可选
case value2 :
//语句
break; //可选
//...
default : //可选
//语句
}
switch-case结构与多种if结构作用相似,但是有局限性。其特点:
- 1、JDK1.7之前的版本中,switch语句支持的数据类型有
byte、short、int、char和枚举类型
(在整数类型中,唯独不支持long类型);在JDK1.7及以后的版本中,增加了对String类型的支持(编译后是把String转化为hash值,其实还是整数)。 - 2、
用于等值判断
。 - 3、break是每种case的结束标志,如无break则会继续走下个case。
- 4、一般最后都会有个default,用来处理匹配不到任何case的情况。
switch使用示例:
int grade=80;
switch(grade){
case 60:
case 70:
System.out.println("你的成绩为60或70分");
break;
case 80:
System.out.println("你的成绩为80分");
break;
default:
System.out.println("你的成绩为60、70、80之外的分数");
break;
}
1.2 循环结构
Java中循环结构有两种:
- 1、for循环
有三部分:初始化操作、判断终止条件表达式及迭代部分。for循环一般用于循环次数已知的情况
。
此外,在JDK1.5中还引入了for-each语法,也很常用,示例:
int array[] = {7, 8, 9};
for (int arr : array) {
System.out.println(arr);
}
- 2、while循环
有两种:
while(判断条件){
循环体
}
do{
循环体
}while(判断语句);
do-while先执行循环体语句,然后进行判断,也就是无论如何会先执行一次循环体语句。
1.3 跳转语句
跳转语句有三种:break、continue、return。此外,异常处理也可以改变程序执行流程。
break有以下作用:
- 在switch中,用来终止一个语句序列。
- 用来退出一个循环。
continue只能用来for、while、do while循环中,用于跳过当前循环,直接进行下一次循环。
return语句使程序控制返回到调用它的地方,也就是:
- 返回方法的返回值;
- 终止当前程序。
示例:
public class SkipTest {
public static void main(String[] args) {
int result = sum(10);
System.out.println("所求的数字之和是:"+result);
}
private static int sum(int num){
int s = 0;
for(int i=1;i<=num;i++){
if(i==3)
continue;
System.out.println("i:"+i);
s=s+i;
if(i==(num-1))
break;
}
return s;
}
}
测试结果:
i:1
i:2
i:4
i:5
i:6
i:7
i:8
i:9
所求的数字之和是:42
这段代码是一个序列的数字之和,不过不需要+3,所以在i == 3时,使用continue,表示跳过此次循环,进入下一次循环。我们也不希望+10,所以在i == (num-1)时,使用了break,直接跳出循环。return的作用就是返回到调用该方法的地方,有返回值的话将返回值返回,无返回值的话则不用返回。
1.4 分支循环相关问题
1.4.1 switch是否能作用在byte上,是否能作用在long上,是否能作用在String上*
在JDK1.5以前,switch(expr)中,expr只能是byte、short、char、int。
从JDK1.5开始,Java中引入了枚举类型,expr也可以是enum类型。
从JDK1.7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的
。
- 为什么只支持上面几种?int、String都可以,为什么不支持long?
原因是switch对应的JVM字节码lookupswitch、tableswitch指令只支持int类型。 byte、char、short类型在编译期默认提升为int,并使用int类型的字节码指令。所以对这些类型使用switch,其实跟int类型是一样的。
1.4.2 continue、break、和return的区别
在循环结构中,当循环条件不满足或循环次数达到要求后,循环会正常结束。但是,有时候可能需要在循环的过程中,提前终止循环,这就是需要用到以下关键词:
- 1、continue
指挑出当前的这一次循环,继续下一次循环。 - 2、break
指跳出整个循环体,继续执行循环下面的语句。 - 3、return
指跳出所在方法,结束该方法的运行。return一般有两种用法:
return;
,直接使用return结束方法执行,用于没有返回值函数的方法;return value;
,return一个特定值,用于有返回值函数的方法。
1.4.3 switch(字符串)的实现原理
反编译前后的结果:
编译后switch还是基于整数,该整数来自于String的hashCode。先比较字符串的hashCode,因为hashCode相同未必值相同,又再次检查了equals是否相同。
1.4.4 if和switch的性能对比
switch的性能比if高。原因是:在switch中只取出了一次变量和条件进行比较,而if中每次都会取出变量和条件进行比较,因此if的效率就会比switch慢很多。并且,分支的判断条件越多,switch性能高的特性体现的就越明显。
- tableswitch和lookupswitch
对于switch来说,最终生成的字节码有两种形态:一种是tableswitch,另一种是lookupswitch,决定最终生成的代码使用那种形态取决于switch的判断添加是否紧凑。例如到case是1…2…3…4这种依次递增的判断条件时,使用的是tableswitch,而像case是1…33…55…22这种非紧凑型的判断条件时则会使用lookupswitch。
当执行一次tableswitch
时,堆栈顶部的int值直接用作表中的索引,以便抓取跳转目标并即执行跳转。也就是说tableswitc 的存储结构类似于数组,是直接用索引获取元素的,所以整个查询的时间复杂度是O(1)
,这也意味着它的搜索速度非常快。
而执行lookupswitch
时,会逐个进行分支比较或者使用二分法进行查询,因此查询时间复杂度是O(logn)
,所以使用lookupswitch会比tableswitch慢。
1.4.5 让if…else更优雅的几种写法
- 1、使用return
使用return可以去掉多余的else。示例:
//优化前
if ("java".equals(str)) {
// 业务代码......
} else {
return;
}
//优化后
if (!"java".equals(str)) {
return;
}
// 业务代码......
- 2、使用Map
使用Map数组,把相关的判断信息,定义为元素信息可以直接避免if…else…判断。示例:
//优化前
if (t == 1) {
type = "name";
} else if (t == 2) {
type = "id";
} else if (t == 3) {
type = "mobile";
}
//优化后
Map<Integer, String> typeMap = new HashMap<>();
typeMap.put(1, "name");
typeMap.put(2, "id");
typeMap.put(3, "mobile");
type = typeMap.get(t);
- 3、使用三元运算符
示例:
//优化前
Integer score = 81;
if (score > 80) {
score = 100;
} else {
score = 60;
}
//优化后
score = score > 80 ? 100 : 60;
- 4、合并条件表达式
有些逻辑判断是可以通过梳理和归纳,变更为更简单易懂的逻辑判断代码。示例:
//优化前
String city = "西安";
String area = "029";
String province = "陕西";
if ("西安".equals(city)) {
return "xi'an";
}
if ("029".equals(area)) {
return "xi'an";
}
if ("陕西".equals(province)){
return "xi'an";
}
//优化后
if ("西安".equals(city) || "029".equals(area) || "陕西".equals(province)){
return "xi'an";
}
- 5、使用枚举
JDK1.5中引入了枚举。示例:
//优化前
Integer typeId = 0;
String type = "Name";
if ("Name".equals(type)) {
typeId = 1;
} else if ("Age".equals(type)) {
typeId = 2;
} else if ("Address".equals(type)) {
typeId = 3;
}
//优化后
public enum TypeEnum {
Name(1), Age(2), Address(3);
public Integer typeId;
TypeEnum(Integer typeId) {
this.typeId = typeId;
}
}
typeId = TypeEnum.valueOf("Name").typeId;
- 6、使用Optional
从JDK 1.8开始引入Optional类,在JDK1.9时对Optional类进行了改进,增加了ifPresentOrElse()方法,可以借助它,来消除if else的判断。示例:
//优化前
String str = "java";
if (str == null) {
System.out.println("Null");
} else {
System.out.println(str);
}
//优化后
Optional<String> opt = Optional.of("java");
opt.ifPresentOrElse(v -> System.out.println(v), () -> System.out.println("Null"));
- 7、梳理优化判断逻辑
即通过分析if…else…的逻辑判断语义,写出更加易懂的代码。示例:
//优化前
// 年龄大于18
if (age > 18) {
// 工资大于5000
if (salary > 5000) {
// 是否漂亮
if (pretty == true) {
return true;
}
}
}
return false;
//优化后
if (age < 18) {
return false;
}
if (salary < 5000) {
return false;
}
return pretty;
需要尽量把表达式中的包含关系改为平行关系,这样代码可读性更好,逻辑更清晰。
- 8、选择性地使用switch
if和switch都能使用的情况下,可以尽量使用switch,因为switch在常量分支选择时,switch性能会比if…else好。示例:
//优化前
if ("add".equals(cmd)) {
result = n1 + n2;
} else if ("subtract".equals(cmd)) {
result = n1 - n2;
} else if ("multiply".equals(cmd)) {
result = n1 * n2;
} else if ("divide".equals(cmd)) {
result = n1 / n2;
} else if ("modulo".equals(cmd)) {
result = n1 % n2;
}
//优化后
switch (cmd) {
case "add":
result = n1 + n2;
break;
case "subtract":
result = n1 - n2;
break;
case "multiply":
result = n1 * n2;
break;
case "divide":
result = n1 / n2;
break;
case "modulo":
result = n1 % n2;
break;
}
1.4.6 循环体中的语句要考量性能
以下操作尽量移至循环体外处理,如定义对象、变量、 获取数据库连接,进行不必要的try-catch操作。这样不仅能保证程序的正确性还能提高程序的执行效率。
二、数组
一个数组就代表在内存中开辟一段连续(意味着有序)
、用来存储相同类型
数据的空间,其特征如下:
- 数组名代表的是连续空间的首地址。
- 通过首地址可以依次访问数组所有元素,元素在数组中的位置叫做下标,从0开始。
- 数组长度一旦声明,不可改变不可追加。
2.1 声明数组
数组的声明方式有两种,以int型数组举例:int[ ] array和int array[ ],一般用第一种方式。示例:
int[ ] array = new int[10]
该语句的意思是创建一个容量大小为10的int型数组,数组中默认值是0。也就是说,当数组元素未赋值时,数组元素的值为数组元素类型的默认值(此处是int型,所以默认值是0)。
2.2 初始化数组
数组初始化的几种形式:
- 1、直接给每个元素赋值
int[ ] array1 = {1,2,3,4,5,6};
- 2、给一部分赋值,后面的都为默认值
int array[4] = {1,2};
- 3、由赋值参数个数决定数组的个数
int[ ] array2 = new int[ ]{1,2,3,4,5,6};
- 4、数组用作可变参数
Java中还有一种数组较冷门的用法是可变参数,示例:
public int add(int... numbers){
int sum = 0;
for(int num : numbers){
sum += num;
}
return sum;
}
add(); // 不传参数
add(1); // 传一个参数
add(2,1); // 传多个参数
add(new Integer[] {1, 3, 2}); // 传数组
2.3 遍历数组
数组的遍历方式也有两种,以int型数组举例。
- 1、for循环
for(int i=0;i<array.length;i++)
System.out.println(array[i]+" ");
- 2、foreach循环
for(int arr:array)
System.out.println(arr+" ");
2.4 数组作为方法参数/方法返回值
数组可以作为参数传递给方法。示例:
//打印int数组中元素
public static void printArray(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
数组也可以数组作为函数的返回值。示例:
public static int[] reverse(int[] list) {
int[] result = new int[list.length];
for (int i = 0, j = result.length - 1; i < list.length; i++, j--) {
result[j] = list[i];
}
return result;
}
2.5 数组复制
对数组进行复制的方法,常常有System.arraycopy和使用Arrays工具类两种方法,这两种方法在JDK源码中经常出现。此处介绍第一种,具体的方法是:
System.arraycopy(src, srcPos, dest, destPos, length)
src: 源数组
srcPos: 从源数组复制数据的起始位置
dest: 目标数组
destPos: 复制到目标数组的起始位置
length: 复制的长度
示例:
static int[] arr1 = {1,2,3,4,5,6};
static int[] arr2 = new int[6];
public static void main(String[] args) {
System.arraycopy(arr1, 0,arr2, 1, 3);
for(int i=0;i<arr2.length;i++) {
System.out.print(arr2[i] + " "); //0 1 2 3 0 0
}
}
2.6 多维数组
多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组,格式:
type[][] typeName = new type[typeLength1][typeLength2];
type可以为基本数据类型和复合数据类型,typeLength1和typeLength2必须为正整数,typeLength1为行数,typeLength2为列数。例如:
int[][] a = new int[2][3];
二维数组 a 可以看成一个两行三列的数组。
给二维数组赋值的示例:
String[][] s = new String[2][];
s[0] = new String[2];
s[1] = new String[3];
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");
2.7 数组在内存中如何分配
对于数组的初始化,有以下两种方式:静态初始化和动态初始化。
- 1、静态初始化
初始化时显式指定每个数组元素的初始值,由系统决定数组长度,如:
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4
String[] computers = {"Dell", "Lenovo", "Apple", "Acer"};
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3
String[] names = new String[]{"多啦A梦", "大雄", "静香"};
- 2、动态初始化
初始化时显式指定数组的长度,由系统为数据每个元素分配初始值,如:
//只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null
String[] cars = new String[4];
因为数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况:
由上图可知,静态初始化方式,虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化方式,虽然没有显示的指定初始化值,但是因为Java数组是引用类型的变量,所以系统也为每个元素分配了初始化值Null。
当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值也是对应的默认值0。
三、字符串
String(字符串),字符串是常量,它们的值在创建之后不能更改
。这个特性可以从String的底层实现看出:
//String的底层实现是字符数组
private final char value[];
每次+
操作 : 隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法拼接字符串
。
3.1 String的创建
常常用来比较的两种创建字符串的方式:
/*创建对象是在方法区的常量池(常用)*/
String str1 = "one";
/*创建对象是在堆内存*/
String str2 = new String("one");
第一种直接赋值的方式,创建的对象是在常量池
;第二种通过构造方法,创建的对象是在堆内存
。
JDK1.7及之后版本的JVM将运行时常量池从方法区中移了出来,在Java堆中开辟了一块区域存放运行时常量池。
这两种变量的比较:
System.out.println(str1==str2); //false
System.out.println(str1.equals(str2)); //true
==比较的是对象的地址,str1对象和str2对象分别在常量池(常量池一般就是指字符串常量池,是用来做字符串缓存的一种机制
)和堆,所以用此方式比较的结果为false。Strng重写了equals方法,比较的是对象的值,所以比较结果为true。
直接赋值创建字符串对象的方式,在创建多个相同内容的对象时,也只开辟一块内存空间,并且会自动入常量池,不会产生垃圾。通过构造方法创建字符串对象的方式,在创建多个相同内容的对象时,会开辟多个内存空间,并且创建的对象不在常量池里。因此,在实际开发过程中,都是使用直接赋值的方法创建字符串对象
。
在字符串使用equals比较时,一般把要比较的对象放前面,避免出现空指针问题,示例:
String str1 = "one";
System.out.println("two".equals(str1));
3.2 String的使用
3.2.1 获取指定位置字符
//方法定义
public char charAt(int index)
//示例:
String str1 = "abcdef";
System.out.println(str1.charAt(1)); //b
从上面可以看出使用charAt时,索引下标是从0开始的,这也从侧面说明了String的底层实现是数组。
3.2.2 字符串比较
- 1、字符串比较(ASCII码)
//方法定义
public native int compareTo(String string)
返回值是两个字符串的ASCII码差值。如果第一个字符和参数的第一个字符不等,结束比较,返回他们之间的差值。如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较。以此类推,直至比较的字符或被比较的字符有一方结束。示例:
String str1 = "one";
String str2 = "one";
String str3 = "on1two";
String str4 = "onetwo";
System.out.println(str1.compareTo(str2)); //0
System.out.println(str1.compareTo(str3)); //52
System.out.println(str1.compareTo(str4)); //-3
System.out.println(str3.compareTo(str4)); //-52
- 2、字符串比较(值)
//方法定义
public boolean equals(Object other)
此处重写了Object的euqals方法,String中重写equals方法的步骤:
- 使用==操作符检查“实参是否为指向对象的一个引用”。
- 使用instanceof操作符检查“实参是否为正确的类型”。
- 把实参转换到正确的类型。
- 对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹配。
- 3、字符串忽略大小写比较(值)
//方法定义
public boolean equalsIgnoreCase(String string)
//示例
System.out.println("asd".equalsIgnoreCase("ASD")); //true
3.2.3 字符串连接
//方法定义
public String concat(String string)
//示例
String str1 = "abcdef";
String str2 = "123";
System.out.println(str1.concat(str2)); //abcdef123
3.2.4 判断是否以指定子串开头/结尾
- 1、是否以指定子字符串开头
//方法定义
public boolean startsWith(String prefix)
//示例
System.out.println("abcde".startsWith("ab")); //true
- 2、是否以指定的子字符串结尾
//方法定义
public boolean endsWith(String suffix)
//示例
String str1 = "abcdef";
String str2 = "123";
String str3 = "def";
System.out.println(str1.endsWith(str2)); //false
System.out.println(str1.endsWith(str3)); //true
3.2.5 检索字符/子字符串的所在位置
- 1、从初始位置,检索字符第一次出现的位置
//方法定义
public int indexOf(int c)
//示例
String str = "asd";
System.out.println(str.indexOf('s')); //1
- 2、从指定位置,检索字符第一次出现的位置
//方法定义
public int indexOf(int c, int start)
//示例
String str = "asdfg";
System.out.println(str.indexOf('s',2)); //-1,代表没检索到
System.out.println(str.indexOf('s',1)); //1
System.out.println(str.indexOf('s',0)); //1
- 3、检索子字符串第一次出现的位置
//方法定义
public int indexOf(String string)
//示例
System.out.println("abcde".indexOf("cd")); //2
- 4、从指定位置,检索子字符串第一次出现的位置
//方法定义
public int indexOf(String subString, int start)
//示例
System.out.println("abcde".indexOf("cd",2)); //2
- 5、从初始位置,检索字符最后一次出现的位置
//方法定义
public int lastIndexOf(int c)
//示例
System.out.println("abcde".lastIndexOf('c')); //2
- 6、从指定位置,检索字符最后一次出现的位置
//方法定义
public int lastIndexOf(int c, int start)
//示例
System.out.println("abcde".lastIndexOf('c',3)); //2
- 7、检索字符串最后一次出现的位置
//方法定义
public int lastIndexOf(String string)
//示例
System.out.println("abcde".lastIndexOf("bc")); //1
- 8、从指定位置,检索字符串最后一次出现的位置
//方法定义
public int lastIndexOf(String subString, int start)
//示例
System.out.println("abcde".lastIndexOf("bc",2)); //1
3.2.6 获取字符串长度
//方法定义
public int length()
//示例
System.out.println("abcde".length()); //5
3.2.7 分割字符串
//方法定义
public String[ ] split(String regularExpression)
//示例:
String str = new String("Y-s-t-e-n");
System.out.println("分隔符返回值 :" );
for (String s: str.split("-")){
System.out.println(s);
}
测试结果:
分隔符返回值 :
Y
s
t
e
n
使用split对字符串string进行分割字符串时,如果str中没有符合规则的,就返回完整的str字符串
。示例:
String str = new String("Y-s-t-e-n");
System.out.println("分隔符返回值 :" );
for (String s: str.split("2")){
System.out.println(s);
}
结果:
分隔符返回值 :
Y-s-t-e-n
3.2.8 截取字符串
- 1、截取子字符串,从指定位置到末尾
//方法定义
public String substring(int start)
//示例
System.out.println("abcde".substring(1)); //bcde
- 2、截取子字符串,从start(包含)到end(不包含),即区间是前开后闭
//方法定义
public String substring(int start, int end)
//示例
System.out.println("abcde".substring(1,3)); //bc
3.2.9 删除字符串的头尾空白符
//方法定义
public String trim()
//示例
System.out.print(" asdfg ".trim()); //asdfg
3.2.10 将不同类型的值转化为字符串
//方法定义
public static String valueOf(primitive data type x)
//示例
long l1 = 1001L;
int i1 = 123;
System.out.println(String.valueOf(l1)); //1001
System.out.println(String.valueOf(i1)); //123
3.2.11 大小写转换
//将字符串转成小写字母
public String toLowerCase()
//将字符串转成大写字母
public String toUpperCase()
3.3 String优化
3.3.1 编译优化
字面量,即final修饰的String变量,都会在编译期被优化,并且会被直接运算好。
1)c和d中,final变量b已经被替换为其字符串常量了。
2)f、g中,b替换为其字符串常量,并且在编译时字符串常量的+运算会被执行,返回拼接后的字符串常量
3)j,a1作为final变量,在编译时被替换为其字符串常量。
解释
:c == h、d == h、e == h为false。c是运行时使用+拼接,创建了一个新的堆中的字符串 ab,与ab字符串常量不是同一个对象;
解释
:f == h、g == h为 true。f编译时进行优化,其值即为字符串常量ab,h 也是,指向字符串常量池中的同一个对象。
3.3.2 intern优化
JDK1.7之后,JVM将字符串常量池放入了堆中,之前是放在方法区。
intern()方法设计的初衷,就是重用String对象,以节省内存消耗。
一定是new得到的字符串才会调用intern,字符串常量没有必要去intern
。
当调用intern方法时,如果池已经包含一个等于此String对象的字符串(该对象由equals(Object) 方法确定),则返回池中的字符串。否则,常量池中直接存储堆中该字符串的引用(1.7 之前是常量池中再保存一份该字符串)。简单来说,intern方法的作用是手动将字符串加入常量池
。
//方法定义
public native String intern();
- 示例1
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);// false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);// true
String s = newString(“1”),生成了常量池中的"1"和堆空间中的字符串对象。
s.intern(),这一行的作用是s对象去常量池中寻找后发现"1"已经存在于常量池中了。
String s2 = “1”,这行代码是生成一个 s2 的引用指向常量池中的“1”对象。
结果就是s和s2的引用地址明显不同。因此返回了 false。
String s3 = new String(“1”) + newString(“1”),这行代码在字符串常量池中生成“1” ,并在堆空间中生成 s3 引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
s3.intern(),这一行代码,是将 s3 中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6 的做法是直接在常量池中生成一个 “11” 的对象。
但是在 JDK1.7 中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说 s3.intern() ==s3 会返回 true。
String s4 = “11”, 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此 s3 == s4 返回了 true。
- 示例2
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);// false
String s3 = new String(“1”) + newString(“1”),这行代码在字符串常量池中生成"1",并在堆空间中生成 s3 引用指向的对象(内容为"11")。注意此时常量池中是没有 “11"对象的。
String s4 = “11”, 这一行代码会直接去生成常量池中的"11”。
s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。
结果就是 s3 和 s4 的引用地址明显不同。因此返回了 false。
- 示例3
String str1 = new String("SEU") + new String("Calvin");
System.out.println(str1.intern() == str1);// true
System.out.println(str1 == "SEUCalvin");// true
str1.intern() == str1 就是上面例子中的情况,str1.intern()发现常量池中不存在“SEUCalvin”,
因此指向了 str1。 "SEUCalvin"在常量池中创建时,也就直接指向了 str1 了。两个都返回true就理所当然啦。
- 示例4
String str2 = "SEUCalvin";//新加的一行代码,其余不变
String str1 = new String("SEU") + new String("Calvin");
System.out.println(str1.intern() == str1);// false
System.out.println(str1 == "SEUCalvin");// false
str2先在常量池中创建了“SEUCalvin”,那么 str1.intern()当然就直接指向了str2,后面的"SEUCalvin"也一样指向 str2。所以谁都不搭理在堆空间中的str1了,所以都返回了false。
3.4 String相关问题
3.4.1 什么是字符串常量池?
字符串常量池位于堆内存中,专门用来存储字符串常量
,可以提高内存的使用率,避免开辟多块空间存储相同的字符串。在创建字符串时,JVM会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
3.4.2 String有哪些特性?
- 1、不变性
String是只读字符串。 - 2、常量池优化
String创建之后,会在字符串常量池中进行缓存,如果下次创建同样的String时,会直接返回缓存的引用。 - 3、final
使用final来定义String类,表示String类不能被继承,提高了系统的安全性。String类利用了final修饰的char类型数组存储字符,源码:
private final char value[];
3.4.3 为什么String在Java中是不可变的*
String作为不可变类有很多优势。
- 1、字符串池(节约内存)
字符串池是特殊存储区域。创建字符串时,如果池中已存在该字符串时,将返回现有字符串的引用,而不是创建新String。 - 2、缓存Hashcode(高性能)
Java中经常会用到字符串的哈希码(hashcode)。例如,在HashMap或HashSet中,字符串的不可变能保证其hashcode保持一致,这样就可以避免一些不必要的麻烦。这也就意味着每次在使用一个字符串的hashcode的时候不用重新计算一次,这样更加高效。在String类中,有以下这段代码:
/** Cache the hash code for the string */
private int hash; // Default to 0
此处的hash变量就是为了保存了String对象的hashcode,因为String类不可变,所以一旦对象被创建,该hash值也无法改变。所以,每次想要使用该对象的hashcode的时候,直接获取即可。
String不可变之后就保证的hash值的唯⼀性,这样它就更加⾼效,并且更适合做HashMap的key- value缓存。
- 3、不可变对象自然是线程安全的(安全)
由于无法更改不可变对象,因此可以在多个线程之间自由共享它们,不存在同步的问题。
总之,String被设计成不可变的主要目的是为了安全和效率。
3.4.4 常量池的使用
示例:
String s1="a"+"b"+"c";
String s2="abc";
System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
创建s1对象时,就在常量池中产生了一个"abc"字符串,所以再用赋值的方式创建相同内容的字符串时,就直接使用常量池中,不再重复创建,所以两种比较方式的结果都是true,因为本就是一个对象。
3.4.5 常规字符串拼接*
示例:
String s1="ab";
String s2="abc";
String s3=s1+"c";
System.out.println(s3==s2); //false
System.out.println(s3.equals(s2)); //true
结果分析:s2是创建在常量池中的,s3是通过字符串拼接生成的,底层是通过StringBuilder(或 StringBuffer)对象实现的,这样产生的对象是存在堆内存中的
。所以两者的地址是不一样的,==比较结果是false;内容是一样的,equals比较结果是true。
3.4.6 特殊字符串拼接
示例:
String s = null;
s += "abc";
System.out.println(s); //nullabc
s += null;
System.out.println(s); //nullabcnull
结果分析:字符串拼接时,会先调用String.valueOf(Object obj)来将对象转换为字符串,该方法的实现是:
public static String valueOf(Object value) {
return value != null ? value.toString() : "null";
}
所以在字符串拼接时,会先判断是不是null,是null的话,会先转换成"null",而不是""进行拼接。
字符串常量池是在编译期确定好的,一次性创建一个完整的字符串
(String s1="zxc"或String s2=“z”+“xc”)时会去常量池查找
。
字符串拼接(如:str+"asd")是在运行期确定的,不会去常量池查找。此时新的对象会分配到堆内存中
,字符串拼接的实际过程:
String s2 = new StringBuilder(str).append("asd").toString()
3.4.7 String s=new String(“abc”) 创建了几个对象*
将创建1或2个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共2个字符串对象。
验证:
String s1 = new String("abc");
String s2 = "abc";
// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的
System.out.println(s1 == s2);
// 输出 true
System.out.println(s1.equals(s2));
3.4.8 equals与==的区别
Java对于eqauls方法和hashCode方法是这样规定的:
- 如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
- 如果两个对象的hashCode相同,它们并不一定相同。
‘==’ 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。
equals用来比较的是两个对象的内容是否相等。所有的类都是继承自Object类,而Object中的equals方法返回的却是==的判断。Object 类中的equals() 方法:
public boolean equals(Object obj) {
return (this == obj);
}
因此,一般的实体类为了equals方法可以正确使用,都要重写equals方法。
- 总结
==对于基本类型来说是值比较,对于引用类型来说是比较的是引用(即判断两个对象是不是同一个对象)。
equals默认情况下是引用比较,只是很多类重新了equals方法,比如String、Integer等把它变成了值比较,所以一般情况下equals比较的是值是否相等。
3.4.9 String str="i"与 String str=new String(“i”)一样吗?
不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中
。
示例:
String x = "asd";
String y = "asd";
String z = new String("asd");
System.out.println(x == y); // true
System.out.println(x == z); // false
String x = “asd” 的方式,Java 虚拟机会将其分配到常量池中。因为常量池中没有重复的元素,所以java虚拟机会先在常量池中检索是否已经有“asd”,如果有那么就将“asd”的地址赋给变量,如果没有就创建一个,然后再赋给变量。
而 String z = new String(“asd”) 则会被分到堆内存中,即使内容一样还是会创建新的对象。
3.4.10 在使用HashMap的时候,用String做key有什么好处?
HashMap内部实现是通过key的hashcode来确定value的存储位置,因为字符串是不可变的,所以当创建字符串时,它的hashcode被缓存下来,不需要再次计算,所以相比于其他对象更快。
3.4.11 String编码,UTF-8和GBK的区别?
GBK编码
:指中国的中文字符,包含了简体中文与繁体中文字符,另外还有一种字符“gb2312”,这种字符仅能存储简体中文字符。
UTF-8编码
:它是一种全国家通用的一种编码。
- GBK和UTF8有什么区别
UTF8编码格式很强大,支持所有国家的语言,正是因为它的强大,才会导致它占用的空间大小要比GBK大。
GBK编码格式,它的功能少,仅限于中文字符,当然它所占用的空间大小会随着它的功能而减少。
3.4.12 数组有没有length()方法?String有没有length()方法?
数组没有length()方法,有length的属性。String有length()方法。
3.4.13 如何实现字符串的反转
示例:
public static String reverse(String originStr) {
if(originStr == null || originStr.length() <= 1)
return originStr;
return reverse(originStr.substring(1)) + originStr.charAt(0);
}
3.4.14 怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?
示例:
String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");
3.4.15 String s=“a”+“b”+“c”+“d”;一共创建了多少个对象
题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个String对象。
String s ="a" + "b" +"c" + "d";
System.out.println(s== "abcd"); //true
3.4.16 String性能提升的几个小技巧
- 1、不要直接+=字符串
StringBuilder使用了char[ ]作为实际存储单元,每次在拼加时只需要修改char[ ]数组即可,只是在toString()时创建了1个字符串;而String一旦创建之后就不能被修改,因此在每次拼加时,都需要重新创建新的字符串,所以StringBuilder.append()的性能就会比字符串的+=性能高很多。 - 2、善用intern方法
善用String.intern() 方法可以有效的节约内存并提升字符串的运行效率。
intern是个高效的本地方法。当调用intern方法时,如果字符串常量池中已经包含此字符串,则直接返回此字符串的引用,如果不包含此字符串,先将字符串添加到常
量池中,再返回此对象的引用。
什么情况下适合使用intern方法?比如要存地址信息,预估需要32G的内存:
public class Location {
private String city;
private String region;
private String countryCode;
private double longitude;
private double latitude;
}
考虑到其中有很多用户在地址信息上是有重合的,比如:国家、省份、城市等,这时就可以将这部分信息单独列出个类,以减少重复。示例:
public class SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
通过优化,数据存储大小减到了20G左右。此时就可以用intern来优化了。示例:
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
这样使用String.intern(),可以使重复性非常高的地址信息存储大小从20G降到几百兆,从而优化了String对象的存储。
从JDK1.7版本以后,常量池已经合并到了堆中,所以不会复制字符串副本,只是会把首次遇到的字符串的引用添加到常量池中。此时只会判断常量池中是否已经有此字符串,如果有就返回常量池中的字符串引用。
- 3、慎重使用Split方法
Split方法在多数情况下使用的是正则表达式,这种分割方式本身没有什么问题,但是由于正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致CPU居高不下。
Java正则表达式使用的引擎实现是NFA(不确定型有穷自动机),这种正则表达式引擎在进行字符匹配时会发生回溯,一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度。
看个例子:
text = "abbc";
regex = "ab{1,3}c";
这是匹配以a开头,以c结尾,中间有1-3个b字符的字符串。
NFA 引擎对其解析的过程:
- 首先,读取正则表达式第一个匹配符a和字符串第一个字符a比较,匹配上了,于是读取正则表达式第二个字符;
- 读取正则表达式第二个匹配符 b{1,3} 和字符串的第二个字符b比较,匹配上了。但因为 b{1,3}表示1-3个b字符串,以及NFA自动机的贪婪特性(也就是说要尽可能多地匹配),所以此时并不会再去读取下个正则表达式的匹配符,而是依旧使用b{1,3}和字符串的第三个字符b比较,发现还是匹配上了,于是继续使用b{1,3}和字符串的第四个字符c比较,发现不匹配了,此时就会发生回溯;
- 发生回溯后,我们已经读取的字符串第四个字符c将被吐出去,指针回到第三个字符串的位置,之后程序读取正则表达式的下一个操作符c ,然后再读取当前指针的下一个字符c进行对比,发现匹配上了,于是读取下一个操作符,然后发现已经结束了。
这就是正则匹配执行的流程和简单的回溯执行流程,而上面的示例在匹配到“com/dzfp-web/pdf/download?request=6e7JGm38jf…”时因为贪婪匹配的原因,所以程序会一直读后面的字符串进行匹配,最后发现没有点号,于是就一个个字符回溯回去了,于是就会导致了CPU运行过高。
所以我们应该慎重使用Split()方法,我们可以用String.indexOf()方法代替Split()方法完成字符串的分割
。如果实在无法满足需求,就在使用Split()方法时,对回溯问题加以重视就可以了。
3.4.17 String类能被继承吗
不可以,因为String类由final修饰符,而final修饰的类是不能被继承的。
String的底层是一个用private和final修饰的char数组。final可以保证数组的的引用地址不会被改变,private不允许外部访问可以保证数组的值不会被修改,这样就能保证String类的不可变性。
- String类不可变性的好处
1、因为String类的不可变性,才能使得JVM可以实现字符串常量池;字符串常量池可以在程序运行时节约很多内存空间,因为不同的字符串变量指向相同的字面量时,都是指向字符串常量池中的同一个对象。这样一方面能够节约内存,另一方面也提升了性能。
2、因为String类的不可变性,从而保证了字符串对象在多线程环境下是线程安全的。
3.5 可变字符串
3.5.1 StringBuffer
StringBuffer是可变字符序列,它是一个类似于String的字符串缓冲区,可以装很多字符串,并且能够对其中的字符串进行各种操作。
StringBuffer是线程安全的,因为其对外提供的方法都有synchronized关键字修饰。
StringBuffer常用方法有:构造方法、追加字符串和toString方法。
- 1、构造方法
public StringBuffer() {
}
public StringBuffer(String string) {
super(string);
}
无参StringBuffer构造方法,默认开辟16个字符的长度的空间。
- 2、追加字符串
//方法定义
public synchronized StringBuffer append(String string)
//示例
String str = "abc";
StringBuffer sb = new StringBuffer(str);
System.out.println(sb.append("def")); //abcdef
- 3、转换为字符串
public synchronized String toString()
3.5.2 StringBuilder
StringBuilder是JDK1.5提出来的。StringBuilder是可变字符序列,也是一个类似于String的字符串缓冲区,可以装很多字符串,并且能够对其中的字符串进行各种操作。
StringBuilder是线程不安全的。
StringBuilder常用方法有:构造方法、追加字符串和toString方法。
- 1、构造方法
public StringBuilder() {
}
public StringBuilder(String str) {
super(str);
}
- 2、追加字符串
//方法定义
public StringBuilder append(String str)
//示例
String str = "abc";
StringBuilder sb = new StringBuilder(str);
System.out.println(sb.append("qwe")); //abcqwe
- 3、转换为字符串
public String toString()
3.5.3 String、StringBuffer和StringBuilder的比较*
三者共同之处:都是final类,不允许被继承。
- 三者不同之处
String | StringBuffer | StringBuilder | 说明 | |
底层实现 | final修饰的不可变字符数组 | 可变的字符数组 | 可变的字符数组 | |
长度是否可变 | 不可变 | 可变 | 可变 | StringBuffer和StringBuilder可以通过append等方法改变长度 |
运行速度 | 慢 | 较慢 | 最快 | String最慢的原因:String为字符串常量,进行值的更改时,都会生成新的String对象; StringBuilder和StringBuffer均为字符串变量,进行值的更改时,是对自身进行更改 |
线程是否安全 | 是(因为是字符串常量) | 线程安全 | 线程不安全 | |
适用情况 | 适用于少量的字符串操作的情况 | 多线程下在字符缓冲区进行大量操作的情况 | 单线程下在字符缓冲区进行大量操作的情况 |
|
String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。
private final char value[];
String的每次+操作 : 隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法拼接+后面的字符。
StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,AbstractStringBuilder的底层是一个可变数组:
/**
* The value is used for character storage.
*/
char[] value;
由于StringBuffer和StringBuilder的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。 另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
3.5.4 StringBuilder在日志框架中的使用
在后端应用开发时,常用用日志框架占位符的形式来打印信息,示例:
log.debug("Hello, {}.", "Debug");
log.info("Hello, {}.", "Info");
log.error("Hello, {}.", "Error");
占位符的底层也是通过StringBuilder拼接来实现的
。
3.5.5 字符串拼接原理
String
:常量,不可变,不适合用来字符串拼接,每次都是新创建的对象,消耗较大;
StringBuffer
:适合用来作字符串拼接,线程安全
;
StringBuilder
:JDK1.5引入,适合用来作字符串拼接,与StringBuffer区别是线程不安全
的。
String拼接时,中间会产生StringBuilder对象(JDK1.5之前产生StringBuffer)。具体的原理是:
两个String(str1、str2)拼接时,首先会调用String.valueOf(obj)方法,这个obj为str1,然后产生StringBuilder, 调用的StringBuilder( )构造方法, 把StringBuilder初始化,长度为str1.length()+16,并且调用append(str1)。接下来调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString()返回结果。
3.5.6 为什么StringBuilder.append()⽅法⽐"+="的性能⾼
StringBuilder⽗类AbstractStringBuilder的实现源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
StringBuilder 使⽤了⽗类提供的 char[] 作为⾃⼰值的实际存储单元,每次在拼加时会修改char[] 数组。
StringBuilder toString() 源码:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
可以看出:StringBuilder 使⽤了 char[] 作为实际存储单元,每次在拼加时只需要修改char[] 数组即可,只是在 toString() 时创建了⼀个字符串;⽽ String ⼀旦创建之后就不能被修改,因此在每次拼加时,都需要重新创建新的字符串,所以 StringBuilder.append() 的性能就会⽐字符串的 += 性能⾼很多。
四、方法
方法是语句的集合,它们在一起执行一个功能;方法是解决一类问题的步骤的有序组合;方法包含于类或对象中。
方法的命名一般用驼峰命名法,即第一个单词应以小写字母作为开头,后面的单词则用大写字母开头写。
方法的优点:
- 使程序变得更简短而清晰。
- 有利于程序维护。
- 可以提高程序开发的效率。
- 提高了代码的重用性。
4.1 方法的定义
一般情况下,定义一个方法包含以下语法:
修饰符 返回值类型 方法名(参数类型 参数名){
...
方法体
...
return 返回值;
}
方法包含一个方法头和一个方法体。下面是一个方法的所有部分:
- 修饰符
修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型。 - 返回值类型
方法可能会返回值。方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果(前提是该方法可能产生结果)。 - 方法名
是方法的实际名称。方法名和参数表共同构成方法签名。 - 参数类型
参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。 - 方法体
方法体包含具体的语句,定义该方法的功能。
4.2 变量作用域
变量的范围是程序中该变量可以被引用的部分。
方法内定义的变量被称为局部变量
。局部变量的作用范围从声明开始,直到包含它的块结束。局部变量必须声明才可以使用。
方法的参数范围涵盖整个方法。参数实际上是一个局部变量。
最常见的局部变量是:for循环的初始化部分声明的变量,其作用范围在整个循环。但循环体内声明的变量其适用范围是从它声明到循环体结束。它包含如下所示的变量声明:
你可以在一个方法里,不同的非嵌套块中多次声明一个具有相同的名称局部变量,但你不能在嵌套块内两次声明局部变量。
五、常用类
5.1 Math类
Math类包含用于执行基本数学运算的方法。最常用的方法是数值计算类方法。
- 1、绝对值
public static double abs(double d)
public static float abs(float f)
public static int abs(int i)
public static long abs(long l)
- 2、对一个数进行上舍入,返回值大于或等于给定的参数,类型为双精度浮点型
//方法定义
public static native double ceil(double d)
//示例
double d = 10.6;
float f = -95;
System.out.println(Math.ceil(d)); //11.0
System.out.println(Math.ceil(f)); //-95.0
- 3、对一个数进行下舍入,返回给定参数最大的整数,该整数小于或等给定的参数
//方法定义
public static native double floor(double d)
//示例
double d = 10.6;
float f = -95;
System.out.println(Math.floor(d)); //10.0
System.out.println(Math.floor(f)); //-95.0
- 4、返回参数的自然数底数的对数值
//方法定义
public static native double log(double d)
//示例
System.out.printf("log(%.3f) 为 %.3f%n",
Math.E*Math.E,Math.log(Math.E*Math.E)); //log(7.389) 为 2.000
- 5、返回double值的底数为10的对数
//方法定义
public static native double log10(double d)
//示例
System.out.printf("log(%.3f) 为 %.3f%n",
100.0,Math.log10(100.0)); //log(100.000) 为 2.000
- 6、返回较大值
public static double max(double d1, double d2)
public static float max(float f1, float f2)
public static int max(int i1, int i2)
public static long max(long l1, long l2)
- 7、返回较小值
public static double min(double d1, double d2)
public static float min(float f1, float f2)
public static int min(int i1, int i2)
public static long min(long l1, long l2)
- 8、返回第一个参数的第二个参数次幂的值
//方法定义
public static native double pow(double x, double y)
//示例
double d1 = 10.0;
double d2 = 2.0;
System.out.println(Math.pow(d1,d2)); //100.0
- 9、返回一个随机数,随机数范围为 [0.0 ,1.0]
public static synchronized double random()
- 10、“四舍五入”,算法为Math.floor(x+0.5) ,即将原来的数字加上 0.5 后再向下取整
//方法定义
public static long round(double d) / static int round(float f)
//示例
System.out.println(Math.round(11.5)); //12
System.out.println(Math.round(-11.5)); //-11
- 11、正平方根
public static native double sqrt(double d)
5.2 Arrays类
Arrays是一个封装好一些对数组操作的类,其中的public方法都是静态的。Arrays的静态方法,有针对不同类型(int、long、short、char、byte、float、double、Object)的重载方法,接下来以int类型为例。Arrays里的方法可以分为以下几大类:
- 1、搜索类
在数组中搜索某个值,返回该值在数组中的下标。该类方法的实现是二分搜索
。
public static int binarySearch(int[] array, int startIndex, int endIndex, int value)
- 2、赋值类
给数组中元素赋予默认值,实现是遍历赋值。
public static void fill(int[] array, int start, int end, int value)
- 3、计算哈希值类
计算数组中元素的哈希值。
public static int hashCode(int[] array)
- 4、比较值类
比较两个数组是否相等。
public static boolean equals(int[] a, int[] a2)
- 5、排序类
该类方法的作用是对数组中的元素进行排序,用到的排序方式是快速排序
。
public static void sort(int[] a)
public static void sort(int[] a, int fromIndex, int toIndex)
- 6、拷贝类
该类方法的作用是拷贝数组中的元素,可以指定起始位置,未指定初始位置的话,默认从0开始。方法实现代码:
public static int[] copyOf(int[] original, int newLength)
public static int[] copyOfRange(int[] original, int from, int to)
- 7、转换为List
asList,示例:
public static <T> List<T> asList(T... a)
5.3 Collections类
Collections类是针对集合操作的工具类。
常用方法:
- 1、排序,默认是自然排序(排序算法是修改的归并排序算法)
使用默认排序规则和自定义规则的示例:
Collections.sort(list);
Collections.sort(list,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if(o1.length()>o2.length()){
return 1;
}else if(o1.length()<o2.length()){
return -1;
}else{
return 0;
}
}
});
- 2、二分查找(前提元素有序)
//二分查找的时候需要先进行排序操作,如果没有排序的话,是找不到指定元素的
public static int binarySearch(List<?> list,T key)
- 3、获取集合中最大值
public static T max(Collection<?> coll)
- 4、获取集合中最小值
public static T min(Collection<?> coll)
- 5、反转集合
public static void reverse(List<?> list)
5.4 Object类
5.4.1 Object中的重要方法
Object是是所有类的父类,如果一个类没有用extends明确指出继承于某个类,那么它默认继承Object类
。
- 1、toString
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
返回类的名字@实例的哈希码的16进制的字符串(即对象地址),示例:
public class JavaTest {
public static void main(String[] args) {
com.test.JavaTest@7852e922
System.out.println((new JavaTest()).toString());
}
}
建议Object的所有子类都重写这个方法
。
- 2、equals
public boolean equals(Object obj) {
return (this == obj);
}
equals方法主要是比较两个对象是否相同,Object中的equals方法比较的是对象的地址是否相同。
String类重写了该方法,改成了比较值是否相同。在实际开发中,对于实体类,一般要重写equals(比较对象的内容而不是地址)和hashCode(尽量让不同的对象产生不同的哈希值)方法。
- 3、hashCode
public native int hashCode();
返回对象的哈希码。示例:
public class JavaTest {
public static void main(String[] args) {
System.out.println((new JavaTest()).hashCode()); //2018699554
}
}
该方法返回对象的哈希码,是一个整数。这个方法遵守以下三个规则:
1、在java程序运行期间,若用于equals方法的信息或者数据没有修改,同一个对象多次调用此方法,返回的哈希码是相同的。
2、如果根据equals方法,两个对象相同,则这两个对象的哈希码一定相同。
3、假如两个对象通过equals方法比较不相同,那么这两个对象调用hashCode也不是要一定不同,相同也是可以的。
- 4、clone
protected native Object clone() throws CloneNotSupportedException;
对象中各个属性的复制,即浅拷贝一个对象,但它的可见范围是protected的。所以实体类使用克隆的前提是:
- 实现Cloneable接口,这是一个标记接口,自身没有方法,这应该是一种约定。调用clone方法时,会判断有没有实现Cloneable接口,没有实现Cloneable的话会抛异常
CloneNotSupportedException。- 覆盖clone()方法,可见性提升为public。
示例:
public class Cat implements Cloneable {
private String name;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Cat c = new Cat();
c.name = "aaa";
Cat cloneCat = (Cat) c.clone();
c.name = "sss";
System.out.println(cloneCat.name);
}
}
- 5、getClass
返回当前运行时对象的Class对象,常用于java反射机制。
public final native Class<?> getClass();
- 6、wait
在Object中存在三种wait方法:
//native方法,不能重写。暂停线程的执行,timeout是等待时间。
public final native void wait(long timeout) throws InterruptedException;
//也是暂停线程的时间,不过多了nanos,这个参数表示额外时间(以毫秒为单位,
//范围是 0-999999),表示超时时间还要加上nanos毫秒。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
//一直等待,没有超时时间
public final void wait() throws InterruptedException {
wait(0);
}
wait()和wait(long timeout, int nanos)都在在内部调用了wait(long timeout)方法。这个方法是线程方面的,在线程相关文章会详细介绍。
- 7、notify/notifyAll
//native方法,不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于锁的概念),如果有多个线程在等待,智慧唤醒其中任意一个。
public final native void notify();
//和notify的区别是会唤醒在此对象监视器上等待的所有线程。
public final native void notifyAll();
这两个方法是唤醒线程。
5.4.2 hashCode与equals*
hashCode()的作用是获取哈希码(散列码);它实际上是返回一个int型整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
。hashCode()定义在Object类中,这就意味着Java中的任何类都包含有hashCode()。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码(可以快速找到所需要的对象)。
equals方法被覆盖过,则hashCode方法也必须被覆盖
。
- Java中的hashCode()的作用
hashCode()的作用是为了提高在散列结构存储中查找的效率,在线性表中没有作用;只有每个对象的hash码尽可能不同才能保证散列的存取性能,事实上Object类提供的默认实现确实保证每个对象的hash码不同(在对象的内存地址基础上经过特定算法返回一个hash码)。
在Java有些集合类(HashSet)中要想保证元素不重复可以在每增加一个元素就通过对象的equals方法比较一次,那么当元素很多时后添加到集合中的元素比较的次数就非常多了。比如集合中现在已经有3000个元素,则第3001个元素加入集合时就要调用3000次equals方法,这显然会大大降低效率。
于是Java采用了哈希表的原理,这样当集合要添加新的元素时,会先调用这个元素的hashCode方法,这样一下子就能定位到它应该放置的物理位置上。如果这个位置上没有元素,则它就可以直接存储在这个位置上而不用再进行任何比较了,如果这个位置上已经有元素了,则就调用它的equals方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次,而hashCode的值对于每个对象实例来说是一个固定值。 - 为什么重写equals时必须重写hashCode方法
如果两个对象相等,则hashcode一定也是相同的。两个对象相等,对两个对象分别调用equals方法都返回true。因此,equals方法被覆盖过,则hashCode方法也必须被覆盖。
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode() ,则该 class的两个对象的hash值无论如何都不会相等(即使这两个对象指向相同的数据)。
- hashCode()与equals()的相关规定
如果两个对象相等,则hashcode一定也是相同的
。两个对象相等,对两个对象分别调用equals方法都返回true
。两个对象有相同的hashcode值,它们也不一定是相等的
。- 因此,
equals方法被覆盖过,则hashCode方法也必须被覆盖
。- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
- 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的
因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的hashCode )。
5.4.3 hashCode与equals的区别*
这两者的区别主要体现在性能和可靠性上。
- 1、equals既然能实现对比的功能,为什么还要hashCode
因为重写的equals方法里实现的比较逻辑一般较复杂,这样效率就比较低;而利用hashCode比较,只比较一个哈希值就可以了,效率较高。 - 2、hashCode效率高,为什么还需要equals
因为hashCode并不是完全可靠。有时不同对象生成的哈希值也会一样,所以hashCode大部分时候可靠,并不是绝对可靠。可以得出:
- equals()相等的两个对象,他们的hashCode()肯定相等,也就是用equals对比是绝对可靠的。
- hashCode()相等的两个对象,他们的equals()不一定先等,也就是hashCode()不是绝对可靠的。
5.4.4 hashCode与equals的相关问题
- 1、为什么Set中存储的对象只要重新equals,就必须重写hashCode?
因为Set中存储的是不重复的对象,需要根据hashCode和equals进行判断,所以要重写这两个方法。
同理,Map中的key也需要重写这两个方法。String重写了这两个方法,所以可以用来用作Map中的key。 - 2、什么时候需要重写?
一般的地方不需要重写,只有当类需要放在HashTable、HashMap、HashSet等hash结构的集合时,才需要重写。 - 3、为什么重写了equals,就要重写hashCode?
如果重写了equals,而没有重写hashCode,就可能导致:两个对象“相等”,但hashCode不一样的情况。此时,当利用这对象作为key保存到HashMap、HashTable、HashSet中时,再用相同的key去查找时,找不到。 - 4、有没有可能两个不相等的对象有相同的hashcode?
有可能。在产生hash冲突时,两个不相等的对象就会有相同的hashcode值。当hash冲突产生时,一般有以下几种方式来处理:
1)拉链法。每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储。
2)开放定址法。一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
3)再哈希。又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个、第三个….等哈希函数计算地址,直到无冲突。 - 5、两个对象的hashCode()相同,则 equals()是否也一定为 true?
equals与hashcode的关系:
- 如果两个对象调用equals比较返回true,那么它们的hashCode值一定要相同;
- 如果两个对象的hashCode相同,它们并不一定相同。
hashcode方法主要是用来提升对象比较的效率,先进行hashcode()的比较,如果不相同,那就不必在进行equals的比较,这样就大大减少了equals比较的次数,当比较对象的数量很大的时候能提升效率。
之所以重写equals()要重写hashcode(),是为了保证equals()方法返回true的情况下hashcode值也要一致,如果重写了equals()没有重写hashcode(),就会出现两个对象相等但hashcode()不相等的情况。这样,当用其中的一个对象作为键保存到hashMap、hashTable或hashSet中,再以另一个对象作为键值去查找他们的时候,则会查找不到。