既然是重新出发, 那一定要更好的出发,我觉得需要对数据结构做一个回顾了。
JAVA中的数据类型
java是强类型语言,每一个变量都需要声明它的类型。
数据类型可以分为两个部分
基本类型
4种整形、2种浮点类型、1种用于表示Unicode编码的字符单元的字符类型char和1种用于表示真值的boolean类型。
每个基本类型都有一个对应的引用类型,称作装箱基本类型(boxed primitive)。
装箱基本类中对应于int、double、boolean的是Integer、Double和Boolean。
引用类型
String和List
Java中的特例
JAVA虽然是面向对象的语言,但是在JAVA8中有8种数据类型不是对象。之所以这样设计,是因为基本数据效率更高。为了深入了解这其中的缘由,有必要去了解一下创建对象的过程。
创建对象的过程
JAVA将内存分为四块区域,堆、栈、静态区和常量区。
- 堆:在RAM中,用于存放java对象
- 栈: 也在RAM中,用于存放引用
- 静态区:位于RAM中,被static修饰的变量存放在静态区。
- 常量区: 常量存在的地方,因为常量永远不会被改变,因此放在ROM中会非常安全。
比如现在创建一个Person对象,实例化一个Person类:
Person p = new Person()
首先,会在堆中开辟一个新空间来存放这个新对象,然后在创建一个引用p放在栈中,这个引用指向堆中的这个新对象,换句话说这个引用是存放这个Person对象的地址。
这样,通过访问p我们也就间接的选到了这个Person.
接下里,我们也许又会再一次new一个Person对象p2
Person p2 = new Person()
这条代码的意思是又创建了一个新的引用也是指向这个对象Person的地址,这个时候如果通过改变Person对象的状态,也会改变p的结果。因为它们指向同一个对象。(String是特例,以后单独笔记)
引用的单独存在是没有意义的,因为它没有一个指向的地址,也就无对象可操作。所以可以只创建一个引用,但是在使用它的时候,必须为它赋值。
一个对象可以有很多个引用,但是一个引用只能对应一个对象。
在java里,‘=’不能被看做是一个赋值语句,它做的只是将一个对象的地址传给左边的引用,使得左边的引用指向了右边的引用所指向的对象。虽然java表面上看已经不存在指针的概念,但是它的引用实质上就是一个指针。所以‘=’不应该被翻译成赋值语句,因为它执行的并不是赋值过程,而是一个传递地址的过程。
特例:基本数据类型
特例存在的意义:使用new创建的对象存在于堆中,而频繁的使用new创建对象尤其是简单的小的变量其实是非常不明智的,因为堆的空间是有限的,如果频繁的操作会导致不可想象的错误。
所以针对这些简单的类型,java采用了c和c++相同的方法,也就是不使用new来创建对象,另外就是创建一个并非是引用的‘自动’的常量。这个变量直接存储‘值’并置于常量区中,因此更加高效。
举个栗子🌰:
int i =2;
int j =2;
需要知道的是,在常量区中,相同的常量只会存在一个,当执行第一行代码时,会先查找常量区中是否有2,没有则开辟一个空间来存放2,然后在栈中存入一个变量i,用来指向这个常量。
执行到第二行,查找发现2已存在,所以就无需开辟新空间,直接在栈中保存一个新变量j,让j也指向2.
(这里我的理解是,堆中存放的对象的引用里存放的事这个对象的地址,而基本数据类型的引用是存放在常量区的这个常量值)
JAVA与此同时也为每一个基本数据类型提供了对应的包装类,使得我们也可以使用new操作符来创建我们想要的变量。
Integer i = new Integer(1);
Integer j = new Integer(2);
使用new操作符每一次都会在堆中开辟新的空间,所以这里i和j指向的是不同的内存地址。
深入了解Integer
再举一个栗子先:
public static void main(String[] args){
Integer a = 5;
Integer b = 5;
System.out.println(a == b);
Integer c = 500;
Integer d = 500;
System.out.println(c == d);
Integer i = new Integer(value:5);
Integer j = new Integer(value:5);
System.out.println(i == j);
}
第一个返回true是很正常的,因为指向的是同一个地址,第二个返回false有点费解下面详说,第三个返回false是因为使用了new关键词来开辟了新的空间,i和j两个对象分别指向堆区中的两块内存空间。
关于第二个问题,可以跟踪一下Integer的源码。
private static class IntegerCache{
static final int low = -128;
static final int hight;
static final Integer cache[];
static{
int h = 127;
String integerCacheHighPropValue = sun.misc.VM.getSavedProperty(s:"java.lang.Integer.IntegerCache.high");
if(integerCacheHighPropValue != null){
try{
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i,127);
h = Math.min(i,Integer.MAX_VALUE - (-low)-1)
}catch(NumberFormatException nfe){
}
}
high = h;
cache = new Integer[(high - low)+1];
int j = low;
for(int k = 0;k<cache.length;k++)
cache[k] = new Integer(j++);
}
assert IntegerCache.high >= 127;
}
说实话我并没有完全搞懂这段代码的意思,但是这是JAVA的一个缓存机制。Integer类的内部类缓存了-128到127的所有数字。(当然Integer类的缓存上限是可以通过修改系统来更改的,了解就好不必深究。)
public static void main(String[] args){
Integer a = 127;
Integer b = 127;
System.out.println(a == b);
Integer a = 128;
Integer b = 128;
System.out.println(a == b);
}
为什么要引入缓存机制
这就回到了为什么要引入基础类型这个特例的问题上了。
这里我留作一个待学习点,以待补充。
另一个特例:String
String是一个特殊的类,它被final修饰符所修饰,是一个不可改变的类。java源码中基本类型的各个包装类也都被final所修饰。这里以String为例。
举一个栗子:
public void testString(){
String s1 = "abc";
String s2 = s1;
String s3 = "abc";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
s1 = "bcd";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
}
执行第一句:常量区开辟空间存放“abc”,s1存放于栈中指向“abc”
执行第二句:s2也指向于“abc”
执行第三句:直接指向“abc”
正因为三个变量都指向于同一个内存地址,结果都为true.
当s1发生变化,新开辟空间存放“bcd”,但是其它变量没有改变指向。
再举一个例子:
public void testString2(){
String s1 = new String("abc");
String s2 = s1;
String s3 = new String("abc");
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s2 == s3);//false
s1 = "abc";
System.out.println(s1 == s2);//false
System.out.println(s1 == s3);//false
System.out.println(s2 == s3);//false
}
执行第一句:在堆里分配空间存放String对象,在常量区开辟空间存放“abc”,String对象指向常量,s1指向该对象
执行第二句:s2也指向上一步new出来的string对象
执行第三句:同执行一,s3指向新string对象,但是new出来的新对象也是指向常量区的该值。
当s1重新赋一个“abc”时,结果却为false了。
这就是String的特殊之处。每一次new出来的string不再是常量,而是一个String对象,因为String类是不可修改的,所以String对象也是不可修改的。
java是值传递还是引用传递的呢?
java是值传递。
public void test(){
int a =2;
int b =3;
swap(a,b);
System.out.print(a);
System.out.print(b);
}
public void swap (int a ,int b){
int temp = a;
a = b;
b = a;
}
我们在嗲用一个需要传递参数的函数时,传给函数的参数并不是参数本身,
我们可以理解为是一个复制。函数使用这个复制来做一些操作,但是并不改变这个数据本身。
传递进去的参数称为形参,而实参并没有发生变化。
可以再看一个复杂一点的例子:
public void testPersonTwo(){
Person p1 = new Person();
p1.setAge(10);
change(p1);
System.out.println();//结果是10
}
private void change(Person p){
p = new Person();
p.setAge(20);
}
可以看到,我们把p1传进去,它并没有被替换成新的对象。因为change函数操作的不是p1这个引用本身,而是这个引用的一个副本。
你依然可以理解为,主函数将p1复制了一份然后变成了chagne函数的形参,最终指向新Person对象的是那个副本引用,而实参p1并没有改变。
再看一个例子:
public void testPersonThree(){
Person p1 = new Person();
p1.setAge(10);
changeAge(p1);
System.out.println(p1.getAge);//结果是20
}
private void changeAge(Person p){
p.setAge(20);
}
简单说明一下,java的传值过程,其实传的是副本,不管是变量还是引用。可以通过引用改变值,但是不能通过副本改变变量。
浮点类型
浮点类型用于表示有小数部分的数值。在Java中有两种浮点类型,一个是4字节的float,一个是8字节的double。我们平时用来编写程序用来表示增长率、物品重量等方面也非常有用。不过,在使用浮点类型时,也需要留意一些问题。
浮点类型只是近似的存储
请问一个问题:0.1+0.2等于多少?是0.3吗?
我们可以看一下java给出的答案
public class Tester{
public static void main(String[] args){
double d1 = 0.1;
double d2 = 0.2;
System.out.print(d1+d2);
}
}
//输出0.30000000000000004
结果似乎有些令人惊讶,这么简单的算术竟然也会算错。
其实,这并不是计算错误,这只是浮点数类型存储的问题。计算机使用二进制来存储数据,而二进制无法准确的表示分数 1/10 ,就像使用十进制时,无法准确地表示 1/3 一样。
数量级差很大的浮点运算
当浮点数值的数量级相差很大的时候,运算又会有什么问题呢?
public class Tester{
public static void main(String[] args){
float f1 = 30000;
float f2 = f1 + 1;
System.out.print(f1+"--"f2);
System.out.print(f1<f2);
float f3 = 30000000;
float f4 = f1 + 1;
System.out.print(f1+"--"f2);
System.out.print(f1<f2);
}
}
//输出 30000 30001.0
//true
//3.0E7 3.0E7 false
又发生了预期外的结果。从输出结果来看,f3竟然和f4是相等的,也就是意味着对f3+1并没有改变f3的值。
这同样是因为浮点数的存储造成的,二进制所能表示的两个相邻的浮点值之间存在一定的空隙。浮点值越大,这个间隙也会越大。当浮点值大道一定程度的时,如果对浮点值的改变很小(例如上面的30000000+1),就不足以使浮点值发生改变。就好比蒸发掉大海中的一滴水,大海还是大海,几乎不存在变化。
如果想要准确的存储,就去使用BigDecimal吧,有必要了解的可以去自行百度,这里就不做过多介绍了,已经是Java封装好的类库了