19. Java基本数据类型、有了基本数据类型,为什么还需要包装类型?
19.1、Java基本数据类型,数值范围
Java共有4类8种基础数据类型:byte、short、int、long、float、double、char、boolean。
1、四种整数类型(byte、short、int、long):byte:8位,用于表示最小数据单位,如文件中数据,-128~127。 short:16位,很少用,-32768 ~ 32767。int:32位,最常用,-231-1~231(21亿)。long:64位,次常用。注意事项:int i=5; //5叫直接量(或字面量),即直接写出的常数。整数字面量默认都为int类型,所以在定义的long型数据后面加L或l。小于32 位数的变量,都按int结果计算。强转符比数学运算符优先级高。
2、两种浮点数类型(float、double):float:32位,后缀F或f,1位符号位,8位指数,23位有效尾数。double:64位,最常用,后缀D或d,1位符号位,11位指数,52位有效尾数。注意事项:浮点数字面量默认都为double类型,所以在定义的float型数据后面加F或f;double类型可不写后缀,但在小数计算中一定要写D或X.X。float的精度没有long高,有效位数(尾数)短。float的范围大于long,指数可以很大。<font color="red">浮点数是不精确的,不能对浮点数进行精确比较。</font>
3、一种字符类型(char):char:16位,是整数类型,用单引号括起来的1个字符(可以是一个中文字符),使用Unicode码代表字符,0~2^16-1(65535)。注意事项:不能为0个字符。转义字符:\n 换行、\r回车、\t Tab字符、\" 双引号、\\表示一个\,两字符char中间用“+”连接,内部先把字符转成int类型,再进行加法运算,char本质就是个数!二进制的,显示的时候,经过“处理”显示为字符。
4、一种布尔类型(boolean):true真和false假。
5、类型转换:char-->自动转换:byte-->short-->int-->long-->float-->double。强制转换:①会损失精度,产生误差,小数点以后的数字全部舍弃。②容易超过取值范围。
19.2、为什么需要包装类型
我们都知道在Java语言中,new一个对象存储在堆里,我们通过栈中的引用来使用这些对象;但是对于经常用到的一系列类型如int,如果我们用new将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,更加高效。
而为什么还需要包装类型呢?Java是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(每一种基本的数据类型都有一种对应的包装类型,如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法(最大值、最小值、null),丰富了基本类型的操作。
另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。
19.3、装箱与拆箱
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
Integer i = 1;自动装箱,实际上在编译时会调用Integer.valueOf()方法来装箱,在Java5之前,只能通过new的方式创建一个Integer对象,Java5之后实现自动装箱。
Integer i = 1;
int j = i; //自动拆箱,实际上也会在编译器调用Integer.intValue();
其他的也类似,比如Double、Character等。
因此可以用一句话总结装箱和拆箱的实现过程:装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。
19.4、Integer的缓存值
首先看下Integer的valueOf()方法:
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。这样做主要的目的就是为了提高效率(因为一些小值数据经常使用)。
注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。Double、Float的valueOf方法的实现是类似的。这主要是因为在某个范围内的整型数值的个数是有限的,而浮点数却不是。
20. hashCode和equals比较、equals与“==”比较
20.1、equals与“==”
1、基本数据类型,也称原始数据类型。
Byte,short,char,int,long,float,double,boolean,他们之间的比较,应用双等号(==),比较的是他们的值。
int a = 10;
long b = 10l;
double c = 10.0;
System.out.println(a == b); // true
System.out.println(a == c); // true
这里比较的时候存在强制类型的转换,低精度自动向高精度转换,然后比较。
2、复合数据类型(类)
当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址, 但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。而是比较指向的对象所存储的内容是否相等。
对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。
20.2、hashCode()是什么
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在JDK的Object类中,这就意味着Java中的任何类都包含有hashCode() 函数。
虽然,每个Java类都包含hashCode()函数。但是,仅仅当创建并某个“类的散列表”(散列表指的是:Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet)时,该类的hashCode() 才有用(作用是:确定该类的每一个对象在散列表中的位置)。其它情况下(例如,创建类的单个对象,或者创建类的对象数组等等),类的hashCode() 没有作用。
也就是说:hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
我们都知道,散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!
散列表的本质是通过数组实现的。当我们要获取散列表中的某个“值”时,实际上是要获取数组中的某个位置的元素。而数组的位置,就是通过“键”来获取的;更进一步说,数组的位置,是通过“键”对应的散列码计算得到的。
下面,我们以HashSet为例,来深入说明hashCode()的作用。
假设,HashSet中已经有1000个元素。当插入第1001个元素时,需要怎么处理?因为HashSet是Set集合,它不允许有重复元素。
“将第1001个元素逐个的和前面1000个元素进行比较”?显然,这个效率是相等低下的。散列表很好的解决了这个问题,它根据元素的散列码计算出元素在散列表中的位置,然后将元素插入该位置即可。对于相同的元素,自然是只保存了一个。
由此可知,若两个元素相等,它们的散列码一定相等; 但反过来确不一定。在散列表中,1、如果两个对象相等(equals),那么它们的hashCode()值一定要相同;2、如果两个对象hashCode()相等,它们并不一定相等。
20.3、hashCode和equals的关系
“hashCode() 和 equals()的关系”分2种情况来说明。
第一种:不会创建“类对应的散列表”
这里所说的“不会创建类对应的散列表”是说:我们不会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,不会创建该类的HashSet集合。
在这种情况下,该类的“hashCode() 和 equals() ”没有半毛钱关系的!这种情况下,equals() 用来比较该类的两个对象是否相等。而hashCode() 则根本没有任何作用,
第二种:会创建“类对应的散列表”
这里所说的“会创建类对应的散列表”是说:我们会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,会创建该类的HashSet集合。
在这种情况下,该类的“hashCode() 和 equals() ”是有关系的:
1)、如果两个对象相等,那么它们的hashCode()值一定相同。 这里的相等是指,通过equals()比较两个对象时返回true。
2)、如果两个对象hashCode()相等,它们并不一定相等。 因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突。
总结:
1、如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
2、如果两个hashCode()返回的结果相等,则两个对象的equals方法不一定相等。
3、如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
因此在重写类的equals方法时,也重写hashcode方法,使相等的两个对象获取的HashCode也相等,这样当此对象做Map类中的Key时,两个equals为true的对象其获取的value都是同一个,比较符合实际。
21. 自增(++)和自减(--)的问题
在java中,a++ 和 ++a的相同点都是给a+1,不同点是a++是先参加程序的运行再+1,而++a则是先+1再参加程序的运行。
举个例子来说:a = 2; b = a++;运行后:b = 2,a = 3;
a = 2; b = ++a;运行后:b = 3,a = 3;
a-- 和 --a情况与 a++ 和 ++a相似,a--为先参加程序运算再-1;--a为先减1后参加运算。
21.1、++和+1的区别
int i = 0;
i = i++;
System.out.println(i);
上述程序输出0;
int i = 0;
i = ++i;
System.out.println(i);
上述程序输出1;
Java采取了中间变量缓存机制!在java中,执行自增运算时,会为每一个自增操作分配一个临时变量,如果是前缀加(++i),就会“先自加1后赋值(给临时变量)”;如果是后缀加(i++),就会“先赋值(给临时变量)后自加1”。运算最终使用的,并不是变量本身,而是被赋了值的临时变量。
i = i++; | i = a++
int temp = i; //先自增1,再使用i的值 | int temp = a;
i = i + 1; | a = a + 1;
i = temp; | i = temp;
i = ++i; | i = ++a
i = i + 1; //先自增1,再使用i的值 | a = a + 1;
int temp = i; | int temp = a;
i = temp; | i = temp;
21.2、a = a+1和a += 1的区别
我们先看一段代码:
byte b = 2;
b = b + 1;
System.out.println(b);
运行结果:错误: 不兼容的类型: 从int转换到byte可能会有损失
报错的原因是short变量在参与运算时会自动提升为int类型,b+1运算完成后变为int,int赋值给short报错。
换成+=的情况:
byte b = 2;
b += 1;
System.out.println(b);
编译通过,输出结果3。
这是因为b += 1并不是完全等价于b = b + 1,而是隐含了强制类型转换,相当于b = (short)(b+1)。
注意:+=不会进行溢出检查
byte b = 127;
b += 1;
System.out.println(b);
输出结果是-128,开发中要特别注意。
22. 值传递与引用传递
值传递是对基本数据类型变量而言的,传递的是该变量值的一个副本,改变副本不影响原变量。(此时内存中存在两个相等的基本类型,即实际参数和形式参数)
引用传递一般是对于对象类型而言的,传递的是该对象地址的一个副本,并不是原对象本身。也就是说实参和形参同时指向了同一个对象,因此在函数中可以通过形参来修改对象中的数据,但是让形参重新指向另一个对象对实参是没有任何影响的。
程序实例:
public class Parameter {
public static void main(String[] args) {
int real = 5;
Obj obj = new Obj("5", 5);
String str = "5";
StringBuffer bf = new StringBuffer("5");
System.out.println("调用方法前:");
System.out.println("real = " + real);
System.out.println("obj = " + obj);
System.out.println("str = " + str);
System.out.println("bf = " + bf);
method(real);
method(obj);
method(str);
method(bf);
System.out.println("调用方法后:");
System.out.println("real = " + real);
System.out.println("obj = " + obj);
System.out.println("str = " + str);
System.out.println("bf = " + bf);
}
public static void method(int para) {
// 基本数据类型,形参的改变不影响实参
para = 10;
}
public static void method(Obj para) {
// 引用数据类型,可以通过相同的引用修改对象内容
para.age = 10;
}
public static void method(String para) {
// 引用数据类型,形参指向新的对象,不影响实参(String类不可变,+操作会产生新对象)
para = para + 10;
}
public static void method(StringBuffer para) {
// 引用数据类型,可以通过相同的引用修改对象内容
para.append(10);
}
}
class Obj {
String name;
int age;
public Obj(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "name:" + name + ",age:" + age;
}
}