本文章是复习Java时整理而成,部分内容不全面。
1. 包装类
包装类(wrapper)是针对八种基本数据类型定义的相应引用类型。从而使得基本数据类型有了类的特点,可以调用类中的方法。对应关系如下:
| 序号 | 基本数据类型 | 包装类 |
|---|---|---|
| 1 | boolean | Boolean |
| 2 | char | Character |
| 3 | byte | Byte |
| 4 | short | Short |
| 5 | int | Integer |
| 6 | long | Long |
| 7 | float | Float |
| 8 | double | Double |
1.1 Boolean、Character
Boolean包装类和Character包装类的层级结构图如下:
1.2 Byte、Short、Integer、Long、Float、Double
Byte、Short、Integer、Long、Float、Double这几个包装类都是整形类型的包装类,其结构层次图如下:
1.3 装箱和拆箱
1.3.1 装箱
所谓的装箱就是将基本数据类型转为对应的包装类。装箱又分为手动装箱和自动装箱(JDK5+) 。以下案例使用int基本类型和Integer包装类进行讲解。其它类型都类似。
手动装箱就是通过Xxx.valueOf(value)方法来由技术人员手动将基本类型value转为包装类。也可以通过new的方式创建一个包装类对象。
//手动装箱
int a = 12;
Integer value1 = new Integer(12);
Integer value2 = Integer.valueOf(a);
在JDK5及其以后新增了自动装箱的功能:
// 自动装箱,调试运行
int i1 = 2;
Integer integer = i1;
自动装箱的本质还是在底层调用了Xxx.valueOf(value)方法,这部分可以通过调试运行查看。
1.3.2 拆箱
拆箱是装箱的逆过程,即将包装类转为基本数据类型。同样,也有手动拆箱和主动拆箱。
手动拆箱是通过使用调用xxx.Value()方法来实现。主动装箱的底层同样是调用了Xxx.value()方法。
//手动拆箱
int v1 = value1.intValue();
int v2 = value2.intValue();
// 自动拆箱
Integer integer =3;
int vv = integer;
1.4 面试题
- 阅读下列代码,写出输出结果?
public static void question(){
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j); // false,
Integer m =1;
Integer n =1;
System.out.println(n==m); // true
Integer x = 128;
Integer y = 128;
System.out.println(x==y); // fasle
Integer i11 = 127;
int i12 = 127;
System.out.println(i11==i12) // true
}
输出结果为:
false
true
false
true
解释如下:
- 第一个输出false,因为是通过
new关键字 创建的两个对象,因此i和j的内存地址不同,输出false。 Integer n = 1的执行本质上是调用了Integer.valueOf(int value)方法,查看valueOf的源码可知:当value的值在128-127之间时,总会缓存,当再次调用valueOf(int value)方法且value的值在-128~127之间时,直接返回缓存中对应数据的地址,而不是创建新的内存地址。- 同理,value的值大于127,则会new一个对象。
- 当
==作用于基本数据类型时,总是比较值是否相等。
2. String类型
在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。String类是final类,其保存的是常量。因此String类是一个不可变类,每次更新都需要重新开辟新的空间。
2.1 创建String对象的两种方式
2.1.1 直接赋值法
String str = "qwe";
使用这种形式创建字符串对象时,先从常量池中查找是否含有“qwe”数据空间,若有,则直接指向该数据空间;若没有,则创建该数据空间,然后指向该数据空间。使用这种方式创建的好处是:避免了相同值的字符串重复创建,节约了内存。 因此,该String对象指向的是“qwe”的数据空间。
2.1.2 调用构造器
String str = new String("qwe");
先在堆中创建空间,里面维护了value属性,然后value属性指向了常量池中的“qwe”空间。若常量池中没有“qwe”空间,,则创建然后指向。因此,该String对象最终指向的时该堆的空间地址。
2.2 String面试题
面试题1:
阅读以下代码,写出输出结果:
public static void test01() {
String str1 = "qwe";
String str2 = "qwe";
System.out.println(str1 == str2); // true
System.out.println(str1.equals(str2)); // true
String s1 = new String("qwe");
String s2 = new String("qwe");
System.out.println(s1 == s2); // fasle
System.out.println(s1.equals(s2)); // true
System.out.println(str1 == s1); // false
System.out.println(str1.equals(s1)); //true
System.out.println(str1 == s1.intern()); // true
System.out.println(s1 == s1.intern()); // false
System.out.println(str1==str1.intern()); // true
}
原因如下:
-
true:两者都是指向的常量池中的“qwe”对象。 -
true:String对象重写了equals方法,用于比较值是否相同。 -
false:是两个对象,因此地址不同。 -
true:String对象重写了equals方法,用于比较值是否相同。 -
false:两者对象地址不同 -
true:同4。比较值。String.intern()方法方法返回的是该String对象在常量池中对应的String对象的地址。先查找,若没有,则创建。
-
ture:str1指向的是在常量池中对应的String对象的地址,s1.intern()方法能够在池中查找到”qwe“对象,因此返回的是该string对象的地址。两者相同。 -
false:s1指向的是在堆中该String对象的地址,而s1.intern()方法是返回的在池中对应的”qwe“对象的地址。两者不同。 -
true: 同7
面试题2:
阅读下面代码,写出运行结果:
public static void test02() {
Person p1 = new Person();
p1.name = "xcy";
Person p2 = new Person();
p2.name = "xcy";
System.out.println("p1.name.equals(p2.name):" + p1.name.equals(p2.name)); // true
System.out.println("p1.name ==p2.name:" + (p1.name == p2.name)); // true
System.out.println("p1.name == "xcy":"+(p1.name == "xcy")); // true
}
原因如下:
true: String对象重写了equals方法,用于比较值是否相同。true:两者的name属性都是通过直接赋值的方式创建的,因此指向的都是常量池中的”xcy“数空间。若将其中一个对象的属性改为使用构造器创建,则输出结果为false。true:两者都是指向的常量池中的xcy数据空间。
2.3 String类的特性
查看String类的源码可知:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {……}
String类是一个final类,其value属性也是final属性,代表不可变的字符串序列。一个字符串对象一旦被分配,则其内容是不能改变的。在修改对象时则会创建一个新的对象,并将原有对象的引用指向新对象的地址。
String s1 = "xcy";
s1 = "coder xcy";
以上代码,执行的过程,创建了两个String对象。
我们再来看几个代码示例:
面试题一: 以下代码执行的过程中,创建了几个String对象?
String a = "hello" + "abc";
按照上面我们分析的逻辑,首先程序需要在常量池中创建hello对象和abc对象,然后创建helloabc对象,最后将该对象的地址引用给a。因此创建了3个对象。这是错误的!
对于编译器来说,它会优化此类型的执行过程:即String a = "hello"+"abc"被优化为了String a = "helloabc"。因此该执行过程只创建了一个对象。
面试题二: 以下代码执行的过程中,创建了几个String对象?画出内存图。
String a = "hello";
String b = "abc";
Stirng c = a+b;
结论:创建了三个对象。
原理(调试运行):
- 在常量池中创建
hello对象 - 在常量池中创建
abc对象 - 创建
StringBuilder对象:StringBuilder sb = new StringBuilder() - 将
hello字符串append到sb对象中:sb.append("hello") - 将
abc字符串append到sb对象中:sb.append("abc") - 调用
sb.toString()方法:String c = sb.toString() - 即c指向的是堆中的一个字符串对象,其value属性指向常量池中
helloabc数据空间。 - 因此,整个过程都创建了
hello、abc、helloabc三个String对象。
面试题三: 阅读下面代码,写出输出结果,并解释原因。
public static void test01() {
String a = "hello";
String b = "abc";
String c = "helloabc";
String d = (a + b).intern();
System.out.println(c == d); // true
System.out.println(c.equals(d)); // true
}
输出结果为:
true
true
由String.intern()方法的原理可知,返回值为常量池中的对应对象。因此,c和d的地址相同。后者是比较的值,因此也相同。
2.4 String 类型的传参方式
面试题四: 下列程序的运行结果是什么?尝试画出内存布局图
class Test02{
String str = new String("xcy");
final char[] ch = {'j','a','v','a'};
public void change(String str,char[] ch){
str = "java";
ch[0] = 'h';
}
public static void main(String[] args) {
Test02 ex = new Test02();
ex.change(ex.str,ex.ch);
System.out.println(ex.str+"and"); // xcyand
System.out.println(ex.ch); // hava
}
}
输出结果如下:
xcyand
hava
原因如下:
在java中,引用数据类型的传参方式是传地址。即在change()方法中,str参数保存的是变量ex.str的引用(地址);ch参数保存的是ex.ch的引用(地址)。而语句str="java"的执行结果则是将str的值(引用地址)替换为"java"字符串的空间地址,但实际上ex.str的引用并未发生改变(因为String类型是不可变类)。因此,ex.str指向的仍是xcy的对象地址。语句ch[0]='h'的执行则是根据ch的引用,将该地址上的0位置的数据替换为了h,ch的引用并未发生改变。
可以简单理解为:
java传递参数都是值,只不过对象传递的是“地址”的值,就是将引用的值复制一份给方法当参数。如果是根据引用把堆里的对象修改了,那么对象真被修改了(如问题代码中的
ch[0]='h') ,不过不是被创建赋值给的那个引用修改的,是方法里的一个复制的引用副本给修改的。如果是将该参数的值(即引用)给覆盖了(如问题代码中的str="java"),那么再怎么修改该参数,原有的实参都不会发生改变。
2.5 String类的常用方法
| 序号 | 方法 | 描述 |
|---|---|---|
| 1 | equals() | 判断内容是否相等,区分大小写 |
| 2 | equalsIgnoreCase() | 判断内容是否相等,不区分大小写 |
| 3 | length() | 获取字符串的长度 |
| 4 | indexOf() | 获取字符(串)在当前字符串中第一次出现的索引,没有则返回-1 |
| 5 | lastIndexOf() | 获取字符(串)在当前字符串中最后一次出现的索引,没有则返回-1 |
| 6 | subString() | 截取指定范围内的字符串,参数为起始索引和介素索引,遵循前闭后开原则 |
| 7 | trim() | 去除前后空格 |
| 8 | charAt() | 获取某个索引位置的字符。 |
| 9 | toUpperCase() | 将字符全部转为大写 |
| 10 | toLowerCase() | 将字符全部转为小写 |
| 11 | replace() | 替换字符串中的指定字符 |
| 12 | split() | 以指定的字符(串)作为分隔符,将字符串进行分割 |
| 13 | compareTo() | 比较两个字符串的大小。逐个比较字符的ASIIC码,返回第一个不同码的差。若一个字符串的开头为另外一个字符串,则返回长度的插值。 |
| 14 | toCharArray() | 将当前字符串转换成字符数组 |
| 15 | fotmat() | 格式化字符串。%s 字符串;%c字符;%d整型;%.2f浮点型(四舍五入) |
3. StringBuffer类
java.lang.StringBuffer类代表的是可变字符串,可以对字符串内容进行增删改。StringBuffer对象的长度是可变的。通过源码可知:StringBuffer继承自StringBuilder类,其value属性不再是final类型,因此是可变的(地址不变)。
是线程安全的。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
……
}
3.1 StringBuffer构造器
3.1.1 StringBuffer()
构造一个空的、初始容量为16的字符串缓冲区对象。
// 源码
public StringBuffer() {
super(16);
}
3.1.2 StringBuffer(CharSequence seq)
构建一个字符串缓冲区对象,它包含与seq相同 的字符。
3.1.3 StringBuffer(int capacity)
构建一个指定容量的字符串缓冲区对象。
public StringBuffer(int capacity) {
super(capacity);
}
3.1.4 StringBuffer(String str)
通过给定的字符串,创建一个字符串缓冲区对象,它包含该字符串。默认长度为,该字符串的长度+16。
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
3.2 String类型和StringBuffer类型的相互转换
- String类型转StringBuffer类型
StringBuffer sBuffer1 = new StringBuffer(str);
StringBuffer sBuffer2 = new StringBuffer();
sBuffer2.append(str);
- StringBuffer类型转为String类型使用
toString()方法即可。
3.3 StringBuffer常用方法
| 序号 | 方法 | 描述 |
|---|---|---|
| 1 | append() | 在当前字符串后追加字符串 |
| 2 | delete(startIndex,endIndex) | 删除指定下标位置的字符串(前闭后开) |
| 3 | replace() | 替换字符串 |
| 4 | indexOf() | 获取字符(串)在当前字符串中第一次出现的索引,没有则返回-1 |
| 5 | insert() | 在指定下表位置插入字符(串) |
| 6 | length() | 获取字串的长度 |
4. StringBuilder类
java.lang.StringBuilder类是一个可变的字符串序列对象。它提供了和StringBuffer类兼容的API。但它不是线程安全的,效率高于StringBuffer。
String、StringBuffer、StringBuilder三者比较
String不可变字符串,效率较低,但是复用性强。StringBuffer:可变字符串,效率较高,是线程安全的。StringBuilder: 可变字符串,效率最高,但不是线程安全的。
5.Math类
在 Java 中 Math 类封装了常用的数学运算,提供了基本的数学操作,如指数、对数、平方根和三角函数等。Math 类位于 java.lang 包,它的构造方法是 private 的,因此无法创建 Math 类的对象,并且 Math 类中的所有方法都是类方法,可以直接通过类名来调用它们。
Math当作工具类使用即可,即查即用,没必要记住其方法。