鉴于昨天的偷懒行为,心里一直不好受,今天痛定思痛,多学点,开始Java常用类的学习吧!
一. 包装类
包装类的引入实例:
在编程语言的学习中,编写一个数据表格是必不可少的操作,比方说要编写一个学生成绩信息表格,里面有多种不同的数据类型:学号(int),姓名(String),年级(int),各科成绩(double[])。在C语言里,用一个顺序表就可以解决了,也就是结构体数组,但是Java并没有结构体,而且Java是面向对象的语言,是不是有自己的方法呢?当然,第一个容易想到的是用一个数组存储。还记得Java的一大特性:多态吗,父类引用指向子类对象。并且我们发现,String类和数组类都有自己的对象,而Object类又是Java继承树的根结点,那么我们就可以利用多态这个性质,利用一个Object数组来存储String类和数组类的对象。好了,现在这两个不同引用数据类型的存储问题解决了,就剩下基本数据类型了。我们也知道,基本数据类型不能创建对象,那我们怎么放到Object数组里呢?于是Java为我们用户编写了基本数据类型的类,它们是用来把不能创建对象的基本数据类型转变成可以创建对象的类。这样基本数据类型也可以变成对象存到Object数组里了。
所以说,Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。 为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。 包装类均位于java.lang包,不用自行导入:
我们打开eclipse,不妨来看看包装类的一些继承关系和源码:
我们先来了解一下“数字型”的包装类。在这八个类中,除了Character和Boolean以外,其他的都是“数字型”,“数字型”都是java.lang.Number的子类。还发现,Number类有abstract修饰,这是个抽象类,那么我们的数字型类就得重写Number类的所有抽象方法,那我们不妨看看有哪些抽象方法:
其中,左上角有A修饰的就是抽象方法。Number类提供的抽象方法有:byteValue()、shortValue()、intValue()、longValue()、floatValue()、doubleValue(),再看看方法的说明,哦,意味着所有的“数字型”包装类都可以互相转型,这些类重写这些抽象方法要实现这样的功能。
好了,对于这些包装类有个初步认识后,就要具体看看它们是怎么定义使用的:
(我们以Integer类作为例子,其他的数字型包装类都一样)
//使用语法:包装类名 对象名=new 构造方法;
// Integer num=new Integer(); //错误的写法!没有无参的构造方法!
Integer i = new Integer(10); //i=10
Integer j = new Integer(50); //j=50
内存分析:
我们都知道创建对象时,要new构造方法,之前在测试时发现new不出无参的构造方法,那我们不妨看看Integer类有哪些构造方法吧:
由于源码是英文,并且要找相应内容比较麻烦,这时我们就可以去查看API。 API,全称Application ProgrammingInterface,即应用程序编程接口。简单理解就是程序员的字典。
注意:看API的步骤一般为: 1.看类所在的包;2. 看类的构造方法;3.其他
看吧,没有无参的构造方法,自然报错。我们发现,居然还可以传一个字符串,这是怎么回事呢,不妨来看看:
懂了,就是传一个纯数值的字符串,然后Java自动帮你变成数字呗:
Integer num=new Integer("123"); //123
Integer num2=new Integer("123a");// 错误写法,包含了除数字外的其他字符
其他Integer包装类常用的成员:
- 静态变量: MAX_VALUE和MIN_VALUE
- 静态方法:valueOf(…)
说明:我们看到,这个方法的作用就是返回一个Integer对象,也就是说我们在创建对象时,可以不直接new出对象,还可以调用该方法创建对象,并且这样做还是官方推荐的写法,所以以后还是用valueOf方法创建实例:
形参radix的意思是表示几进制。因为数字基本上全是十进制,一般很少用。(默认的就是十进制)
Integer int1 = new Integer(20);
Integer int2 = Integer.valueOf(20); // 两种都可,官方推荐这种写法
- 静态方法:parseInt(String s) 和 成员方法: toString()
它们两个方法用于字符串和Integer对象的相互转换。
举例:
//把字符串变成Integer类对象
Integer int3 = Integer.parseInt("334"); //334
Integer int4 = new Integer("999"); //999
/*把Integer类对象变成字符串。
*注:toString()方法返回值是个字符串 */
String str1 = int3.toString(); //字符串"999"
其实之前传字符串的构造方法内部就是使用了parseInt(…)方法
- 成员方法:byteValue()、shortValue()、intValue()、longValue()、floatValue()、doubleValue()
这些都是方法重写父类Number的抽象方法,以longValue()举例:
作用:将Integer类的对象转化成相对应的基本数据类型。
再来到的内容是——自动装箱和自动拆箱:
自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。JDK1.5后,Java引入了自动装箱(autoboxing)/拆箱(unboxing)。
自动装箱:
基本类型的数据处于需要对象的环境中时,会自动转为“对象”。
我们以Integer为例:在JDK1.5以前,这样的代码 Integer i = 5 是错误的,必须要通过Integer i = new Integer(5) ; 这样的语句来实现基本数据类型转换成包装类的过程。而在JDK1.5以后,Java提供了自动装箱的功能,因此只需Integer i = 5;这样的语句就能实现基本数据类型转换成包装类,这是因为JVM为我们执行了Integer i = Integer.valueOf(5); 这样的操作,这就是Java的自动装箱。
自动拆箱:
每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用intValue()、doubleValue()等转型方法。如 Integer i = 5;int j = i; 这样的过程就是自动拆箱。
自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作。有了这样的功能,会给我们编写代码提供很大的方便。如在最开始的引子,写一个学生成绩信息表格:
int[] grade1 = {100,100,100};
Object[] stu1= {1,"zhang",9,grade1}; //1和9都是基本数据类型,Java编译器为用户自动装箱成Integer对象了。不用自己敲!
int[] grade2= {60,60,60};
Object[] stu2= {2,"张坚波",9,grade2};
//用一个二维数组包装一下
Object[][] stu=new Object[2][];
stu[0]=stu1;
stu[1]=stu2;
接下来学习包装类的最后一个内容:包装类的缓存问题。
整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。
缓存处理的原理为:如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。
这个特别像计算机内存中的cache,把最活跃的部分存放到cache中,以后要访问这些内容会更快,就提高运行效率!Java认为-128~127之间的值就是整型数字的最活跃部分!在遇到大量重复数字时,都指向同一个对象,这样既节省了内存,又提高了效率。相关源码如下:
来个代码测试一下:
Integer a1=1;
Integer a2=1;
System.out.println(a1==a2); //true
Integer a3=1000;
Integer a4=1000;
System.out.println(a3==a4); //超出范围 false
我们知道“==”这个符号,在比较引用数据类型时是比较地址的,说明a1和a2都指向的是同一个地址,也就说明了cache数组的存在!
二. String类
从最开始的学习我们就已经知道——Java的字符串是对象,是String类的实例。我们也多次使用过了,今天我们就来深入学习一下String类。
Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。 Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例(不管你有没有用new创建对象)。
String类位于java.lang包中,Java程序默认导入java.lang包下的所有类。
String类的实例创建方法:
- 省略new关键字的直接赋值
String s = "" ; // 空字符串,这种写法完全没意义
String s2 = " Hello World ";
String s3 = "Hello" + "World"; /*可以使用字符串连接符“+”来创建对象。
*注意:这种创建形式编译器会直接进行字符串的拼接
*相当于 String s3="HelloWorld";
*这两个写法是一致的
*/
String s4="Hello";
String s5="World";
String s6=s4+s5; //这样编译器是不可能做到优化的,只有先有s4和s5才有s6
- 用构造器创建对象
这就要看String 类有哪些构造方法了(只列出常见的):
String str=new String(); //空字符串,这个构造方法谨慎使用,原因下面学。
String str2=new String("abc"); //最常用的构造方法
接下来要讲的是String类最重要的一点,关键!String类是不可变字符序列,String类对象即不可变对象。那什么叫做“不可变对象”呢?指的是对象内部的成员变量的值无法再改变。这一特性与String类源码有关:
String类的底层就是一个字符数组实现(Java9之前,<我下的JDK1.8>,Java9之后改为byte[]数组存储,但实质没改变,最后存每个字符还是要变成Unicode编码形式)。这个字符数组又加上了final修饰,所以是不可变的!(另外提一句,String类也是final修饰的,不能被继承)
我们来测试一下这个性质:
String str= new String("a");
str="abc"; //"赋上新值"
System.out.println(str);
看看运行结果:
嗯?这不是打脸吗?这不是可以变的吗?别急,慢慢解释,一句一句来。
第一步:声明str局部变量,存在栈帧的局部变量表里
第二步:利用new调用构造方法,在堆中创建对象(构造方法入栈省略)
第三步:str=“abc”; 实际上是str指向了另外一个对象
所以说,从表面上看,str这个对象的内容改变了,其实不对,是str指向了一个新的对象,原来那个对象的内容并没有改变。总结一下,不可变字符序列不是说不能再“赋新值”了,而是不能改变原有的对象里的内容!!!
因此,重点来了!比如,我们在需要大量拼接字符串时(操作改变字符串内容),使用了String类的对象,这个过程会创建大量无用对象,从而造成内存泄漏!举个例子(抄袭一下教材的例子):
public class Test {
public static void main(String[] args) {
/**使用String进行字符串的拼接*/
String str8 = "";
//本质上使用StringBuilder拼接, 但是每次循环都会生成一个StringBuilder对象
long num1 = Runtime.getRuntime().freeMemory();//获取系统剩余内存空间
long time1 = System.currentTimeMillis();//获取系统的当前时间
for (int i = 0; i < 5000; i++) {
str8 = str8 + i;//相当于产生了10000个对象
}
long num2 = Runtime.getRuntime().freeMemory();
long time2 = System.currentTimeMillis();
System.out.println("String占用内存 : " + (num1 - num2));
System.out.println("String占用时间 : " + (time2 - time1));
/**使用StringBuilder进行字符串的拼接*/
StringBuilder sb1 = new StringBuilder("");
long num3 = Runtime.getRuntime().freeMemory();
long time3 = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
sb1.append(i);
}
long num4 = Runtime.getRuntime().freeMemory();
long time4 = System.currentTimeMillis();
System.out.println("StringBuilder占用内存 : " + (num3 - num4));
System.out.println("StringBuilder占用时间 : " + (time4 - time3));
}
}
这里的String类是不可变字符序列,而StringBuilder类是可变字符序列(之后学),我们来看看它们运行的时间和占用内存的比较:
差距显而易见!所以以后在处理字符串时,一定要注意自己使用的字符串类合不合适!
好了,继续学习一下String类中的一些常用方法:
- 成员方法:charAt(int index)
说明:返回索引处的字符,如果超出范围就会报错。
举例:
String str="abc";
System.out.println(str.charAt(1)); //b
System.out.println(str.charAt(3)); //错误写法,程序会抛出异常
- 成员方法: length()
说明:返回字符串的长度。
举例:
String str="abc";
System.out.println(str.length()); //3
- 成员方法: equals(Object anObject)
说明:运用了多态性,比较两个字符串内容是否一致,一样返回true,否则返回false。
举例:
String str1=new String("abc");
String str2=new String("abc");
System.out.println(str.equals(str2)); //true
System.out.println(str1==str2); //false
- 成员方法: equalsIgnoreCase(String anotherString)
说明:和equals()方法差不多,忽略英文大小写的比较。
- 成员方法: indexOf(String str)
说明:在原字符串中寻找给定字符串,从头开始找,并返回第一次出现的第一个字母的索引,如果不包含这个给定字符串,返回-1。
举例:
String s1="abc";
System.out.println(s1.indexOf("bc")); //1
System.out.println(s1.indexOf("bcccc")); //-1
- 成员方法: substring(int beginIndex,int endIndex)
说明:截取当前字符串的两个索引位置的子字符串( 区间:[begin,end) 左闭右开),并返回。这个子字符串是一个新串,原来的字符串并没有发生改变。并且,substring是全小写的,因为这是一个单词,是子串的意思。
举例:
String s1="abcd";
System.out.println(s1.substring(1,2)); //"b"
System.out.println(s1); //"abcd"
- 其他常见成员方法及说明:
String类相关知识还有最后一个关键点,也是十分重要的——常量池!
在Java的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:
- 全局字符串常量池(String Pool)
- class文件常量池(Class Constant Pool)
- 运行时常量池(Runtime Constant Pool)
现阶段我们不要求深入,只需要知道以下特点:
String s1="abc";
String s2=new String("abc");
第一:类加载完成时,Java会将所有双引号里的内容放到常量池里。
第二: 当遇到类似 String s1=“abc”; 的语句时,对象直接指向常量池中的对应字符串。
第三: 在遇见类似 String s2=new String(“abc”); 语句时,先在堆里创建对象,然后判断该对象存储的具体字符串有没有在常量池里。如果在,对象的内容存放常量池中对应字符串的地址;如果没在,在常量池中放入该字符串,再指过去。(常量池有没有在堆里咱们暂且不管)
举例:
String n1="abc";
String n2="abc";
String n3=new String ("abc");
//看是否为同一个对象
System.out.println(n1==n2); // true
System.out.println(n1==n3); // false
System.out.println(n1==n3.intern());/* true
*intern()方法是用来返回堆中对象存的地址指向常量池的字符串的地址
*
*/