数据类型分为两类:基本类型(Primitive Type) 和引用类型(Reference Type)。
名称 | 含义 | 数据分配 |
---|---|---|
基本类型 | 基本类型包括boolean类型和数值类型。数值类型有整数类型和浮点类型。整数类型包括 byte、short、int、long、char,浮点类型包括float和double。 | 原始数据类型变量的“变量分配”与“数据分配”是在一起的(都在方法区或栈内存或堆内存) |
引用类型 | 引用类型包括类、接口和数组类型,还有一种特殊的null类型。引用数据类型就是对一个对象的引用,对象包括实例和数组两种。实际上,引用类型变量相当于一个指针,但引用类型只能赋值运算。 | 引用数据类型变量的“变量分配”与“数据分配”不一定是在一起的,只要是引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址 |
public class Main{
public static void main(String[] args){
//基本数据类型
int i=1;
double d=1.2;
//引用数据类型
String str="helloworld";
}
}
一个字节等于 8 位。一位(one bite)在计算机里只有 0 或 1 两个值。所以位数就决定了基本数据类型的表数范围。 值得注意的是字符串不是基本数据类型,字符串是一个类,也就是引用数据类型。
名称 | 类型 | 所占字节 | 取值范围 | 默认值 |
---|---|---|---|---|
布尔型 | boolean | 没有给出精确的定义(1或4) | true或false | false |
字节型 | byte | 1 | -128——127 | 0 |
整型 | int | 4 | -231——231-1 | 0 |
短整型 | short | 2 | -215——215-1 | 0 |
长整型 | long | 8 | -263——263-1 | 0 |
字符型 | char | 2 | – | \u0000 |
单精度浮点型 | float | 4 | -2128——2128 | 0.0F |
双精度浮点型 | double | 8 | -21024——21024 | 0.0D |
1、自动类型转换
2、强制类型转化
3、表达式类型的自动提升
//定义一个short类型变量
short sValue = 5;
//表达式中的sValue将自动提升到int类型,则右边的表达式结果类型为int
//将一个int类型赋给short类型的变量将发生错误。
sValue = sValue - 2;
1、装箱过程
名称 | 装箱过程 |
---|---|
Integer、Short、Byte、Character、Long | 即通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,会返回常量池(如IntegerCache.cache)中事先已经创建好的对象;否则创建一个新的Integer对象。注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的 |
Double、Float | 而Double类的valueOf方法采用了与Integer类的valueOf方法不同的实现。因为在某个范围内的整型数值的个数是有限的,而浮点数却不是。。Double、Float的valueOf方法的实现是类似的。 |
Boolean | Boolean类的valueOf直接返回事先创建好的对象。Boolean 中定义了 2 个静态成员属性 public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); |
2、拆箱过程
3、谈谈Integer i = new Integer(xxx)和Integer i =xxx;
4、特别注意
1、值传递和引用传递的区别
/**
* 基本数据类型是值传递
**/
public void add1(int x, double y) {
x = 10000;
y = 10000.0;
}
/**
* 基本数据包装类型和String类型是引用传递,
* 但是由于他们的value都是final修饰的,
* 数据一旦写入就无法更改,
* 所以给人的感觉就像值传递
**/
public void add2(Integer x, Double y, String s) {
x = 10000;
y = 10000.0;
s = s + "add";
}
/**
* 自定义数据类型属于引用传递,
* 引用指向的值被修改
**/
public void add3(Point newPoint) {
newPoint.setMember_int(10000);
}
/**
* 自定义数据类型属于引用传递,
* 但是这里引用指向的地址变了,
* 所以值不变
**/
public void add4(Point newPoint) {
newPoint = new Point();
newPoint.setMember_int(10000);
}
}
package com.study.notes.threads.safe.variable;
/**
* 值传递:
* 传递对象的一个副本,
* 即使副本被改变,
* 也不会影响源对象,
* 因为值传递的时候,
* 实际上是将实参的值复制一份给形参。
*
* 引用传递:
* 传递的并不是实际的对象,
* 而是对象的引用,
* 外部对引用对象的改变也会反映到源对象上,
* 因为引用传递的时候,
* 实际上是将实参的地址值复制一份给形参。
* 说明:对象传递(数组、类、接口)是引用传递,原始类型数据(整形、浮点型、字符型、布尔型)传递是值传递。
*
* @author: lzq
* @create: 2023-07-20 09:42
*/
public class MainDemo {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Point point1 = new Point();
Point point2 = new Point();
int intX = 1;
double doubleY = 1.0;
Integer integerX = 1;
Double doubleEY = 1.0;
String str = "1";
System.out.println("->线程名称:" + Thread.currentThread().getName() +
"-> 改变前 intX:" + intX +
"-> 改变前 doubleY:" + doubleY +
"-> 改变前 integerX:" + integerX +
"-> 改变前 doubleEY:" + doubleEY +
"-> 改变前 str:" + str +
"-> 改变前 point1:" + point1 +
"-> 改变前 point2:" + point2);
myRun.add1(intX,doubleY);
myRun.add2(integerX,doubleEY,str);
myRun.add3(point1);
myRun.add4(point2);
System.out.println("->线程名称:" + Thread.currentThread().getName() +
"-> 改变后 intX:" + intX +
"-> 改变后 doubleY:" + doubleY +
"-> 改变后 integerX:" + integerX +
"-> 改变后 doubleEY:" + doubleEY +
"-> 改变后 str:" + str +
"-> 改变后 point1:" + point1 +
"-> 改变后 point2:" + point2);
}
}
2、方法里面的引用是方法私有的,引用之间的赋值,只是地址传递
方法里面的引用是方法私有的,引用之间的赋值,只是地址传递,只改变地址,可以不会对堆里面的对象有何影响
public static void main(String[] args) {
User user1 = new User("zhangsan",20);
User user2 = new User("lisi",22);
System.out.println("交换前user1:" + user1 + "-》 user2:" + user2);
swap(user1,user2);
System.out.println("交换后user1:" + user1 + "-》 user2:" + user2);
}
private static void swap(User user1, User user2) {
User tmp = v;
user1 = user2;
user2 = tmp;
}
结果:
交换前user1:name:zhangsan age:20-》 user2:name:lisi age:22
交换后user1:name:zhangsan age:20-》 user2:name:lisi age:22
结果分析:
执行swap方法:
swap方法结束后,临时副本user1和user2被回收
1、String
String a = "aaa"; //==> a----->new String("aaa")
String b = a; //==> b----->a, 传引用
b = "bbb"; //==> b----->new String("bbb"), 传引用,b指向了一个新的字符串,a并没有变。
public static void changeStr(String str) {
str = "welcome";
}
public static void main(String[] args) {
String str = new String("1234"); //使用String str = "1234";是一样的效果
changeStr(str);
System.out.println(str);
}
// 最终会输出1234
String是不可改变类(记:基本类型的包装类都是不可改变的)的典型代表,也是Immutable设计模式的典型应用。
Immutable模式的实现主要有以下两个要点:
1. 除了构造函数之外,不应该有其它任何函数(至少是任何public函数)修改任何成员变量。
2. 任何使成员变量获得新值的函数都应该将新的值保存在新的对象中,而保持原来的对象不被修改。
String被修饰为final,所以是不可继承的。String变量一旦初始化后就不能更改,禁止改变对象的状态,从而增加共享对象的坚固性、减少对象访问的错误,同时还避免了在多线程共享时进行同步的需要。因此用“+”进行字符串拼接的时候,实际上新建了一个字符串对象。String的不可变性导致字符串变量使用+号的代价很大。
String类是跨应用程序域的,可以在不同的应用程序域中访问到同一String对象
2、String s = "abc" 和 String s = new String("abc")
JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。
一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象。
3、String中==与equals的区别
因为java所有类都继承于Object基类,而Object中equals用==来实现,所以equals和==是一样的,都是比较对象地址,java api里的类大部分都重写了equals方法,包括基本数据类型的封装类、String类等。对于String类==用于比较两个String对象的地址,equals则用于比较两个String对象的内容(值)。
String s0 =new String ("abc");
String s1 =new String ("abc");
System.out.println(s0==s1); //false 可以看出用new的方式是生成不同的对象
System.out.println(s0.equals(s1)); //true 可以看出equals比较的是两个String对象的内容(值)
4、字符串常量池的使用
字符串常量池(String Pool)保存所欲字符串字面量(literal strings),这些字面量在编译时期就确定,不仅如此,还可以使用String的intern()方法在运行时添加到常量池中。
常量池的使用主要有两种方法: 直接使用双引号声明出来的String对象会直接存储到常量池中。 使用String提供的intern()方法时,会查询字符串常量池中是否存在当前字符,如果不存在则将当前字符串放入常量池中。
String s0 = "abc";
String s1 = "abc";
System.out.println(s0==s1); //true 可以看出s0和s1是指向同一个对象的。
1.new String在堆上创建字符串对象。
2.通过字面量赋值创建字符串(如:String str=”1”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
3.常量字符串的+“”操作,编译阶段直接会合成为一个字符串。如String str=”JA”+”VA”,在编译阶段会直接合并成语句String str=”JAVA”,于是会去常量池中查找是否存在”JAVA”,从而进行创建或引用。
5、编译期确定
分析:因为例子中的 s0和s1中的"helloworld”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而"hello”和"world”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中"helloworld”的一个引用。所以我们得出s0==s1==s2;
String s0="helloworld";
String s1="helloworld";
String s2="hello" + "word";
System.out.println( s0==s1 ); //true 可以看出s0跟s1是同一个对象
System.out.println( s0==s2 ); //true 可以看出s0跟s2是同一个对象
分析:s1字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量 池中或嵌入到它的字节码流中。所以此时的"a" + s1和"a" + "b"效果是一样的。
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = true
6、编译期无法确定
分析:用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池中"helloworld”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象"helloworld”的引用,s2因为有后半部分new String(”world”)所以也无法在编译期确定,所以也是一个新创建对象"helloworld”的引用;
String s0="helloworld";
String s1=new String("helloworld");
String s2="hello" + new String("world");
System.out.println( s0==s1 ); //false
System.out.println( s0==s2 ); //false
System.out.println( s1==s2 ); //false
分析:JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + s1无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2。所以上面程序的结果也就为false。
String s0 = "ab";
String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
7、String的不可变性导致字符串变量使用+号的代价
String s = “a” + "b” + "c”;
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + s2 + s3;
如下的代码也能验证:
long start = 0L;
long end = 0L;
String str = "Hello";
String str1 = "Hello";
String str2 = "World";
start = System.currentTimeMillis();
for (int i = 0; i < 99999; i++) {// 进行十万次循环
str = str+str2;
}
end = System.currentTimeMillis();
System.out.println("使用string的时间是:" + (end - start) + "毫秒!");
结果为:使用string的时间是:14529毫秒!
但是,改一改代码把str=str+str2改为str=str1+str2。代码如下:
start = System.currentTimeMillis();
for (int i = 0; i < 99999; i++) {// 进行十万次循环
str = str1 + str2;
}
end = System.currentTimeMillis();
System.out.println("使用string的时间是:" + (end - start) + "毫秒!");
结果就会变为:使用string的时间是:8毫秒!
8、String、StringBuffer、StringBuilder
因为相对StringBuffer,StringBuilder没有在方法上使用 synchronized 关键字。Synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
名称 | 是否可变 | 效率 | 线程安全 | 使用场景 |
---|---|---|---|---|
String | 不可变字符串 | 在字符串内容不经常发生变化的业务场景优先使用String类。例如:常量声明、少量的字符串拼接操作等。 | ||
StringBuffer | 可变的字符序列 | 效率低 | 线程安全 | 在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBuffer,例如XML解析、HTTP参数解析与封装 |
StringBuilder | 可变的字符序列 | 效率高 | 线程不安全 | 在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder,例如SQL语句拼装、JSON封装等。 |
常量池包含:8种基本数据类型(byte、short、int、float、long、double、char、boolean)、
部分包装类(Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现)、
对象型(如String及数组)还包含一些以文本形式出现的符号引用,
其中Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。