二、数据类型
基本类型
类型说明
类型 | 单位(Byte) | 取值范围 |
---|---|---|
byte | 1 | [128~127] |
short | 2 | [-32768~32767] |
int | 4 | [-2147483648~2147483647] |
char | 2 | [\u0000~\uFFFF]:注意加’ ’ |
float | 4 | [3.402823e+38 ~ 1.401298e-45]:e+38表示是乘10的38次方 |
double | 8 | [1.797693e+308~ 4.9000000e-324] |
long | 8 | [-9223372036854774808~9223372036854774807] |
boolean | 1 | boolean 只有两个值:[true、false],可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。 |
- 整型:其中byte、short、int、long都是表示整数的,只不过他们的取值范围不一样
- float和double:是表示浮点型的数据类型,他们之间的区别在于他们的精确度不同。double型比float型存储范围更大,精度更高,所以通常的浮点型的数据在不声明的情况下都是double型的,如果要表示一个数据是float型的,可以在数据后面加上“F”。浮点型的数据是不能完全精确的,所以有的时候在计算的时候可能会在小数点最后几位出现浮动,这是正常的。
- char型:用于存放字符的数据类型,占用2个字节,采用unicode编码,它的前128字节编码与ASCII兼容
默认值
Data Type | Default Value (for fields) |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | ‘\u0000’ |
String (or any object) | null |
boolean | false |
类型转换
将一种类型的值赋值给另一种类型是很常见的。在Java中,boolean 类型与其他7中类型的数据都不能进行转换,这一点很明确。
但对于其他7种数据类型,它们之间都可以进行转换,只是可能**(强制转换)会存在精度损失或其他一些变化**。
转换分为自动转换和强制转换:
- 自动转换(隐式):无需任何操作。
- 强制转换(显式):需使用转换操作符(type)。
将6种数据类型按下面顺序排列一下
double > float > long > int > short > byte
扩展
- 原始数据类型
- Java® 虚拟机规范
包装类型
Primitive type(原始类型) | Wrapper class(包装类) |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
java中已经有了八种基本数据类型,为什么要使用包装类呢?
当我们需要在 Java 中使用基本数据类型(如 int、double、boolean 等)时,有时候会遇到一些情况,比如:
- 集合框架的限制:Java 中的集合类(如 ArrayList、HashMap 等)只能存储对象,无法直接存储基本数据类型,因为集合中存储的是对象的引用。因此,如果想在集合中存储基本数据类型,就需要使用包装类将基本数据类型转换为对象。
- 泛型类型参数:在使用泛型时,泛型类型参数只能是类类型,无法使用基本数据类型。所以如果想要在泛型中使用基本数据类型,同样需要使用包装类。
- Null 值表示:有时候需要在对象中表示一个“空”或“不存在”的值,在基本数据类型中无法直接表示这种状态。而包装类可以通过 null 来表示这种状态,因为包装类是对象,可以为 null。
- 提供更多功能:包装类提供了一些实用的方法来操作基本数据类型,例如将字符串转换为基本数据类型、比较两个基本数据类型的值、将基本数据类型转换为字符串等。
综上所述,使用包装类可以使基本数据类型具备对象的特性,从而更灵活地应用于 Java 的各种场景中。
自动装箱和拆箱
自动装箱和拆箱让开发人员可以编写更清晰的代码,使其更易于阅读。
什么是自动装箱和拆箱?
自动装箱是Java编译器在基本类型与其相应的对象包装类之间的自动转换。
例如,将int
转换为Integer
,将double
转换为Double
,等等。如果转换以另一种方式进行,则称为拆箱。
基本上有适合的包装类型,就可以进行相互转换,使用自动拆箱装箱完成。
Integer x = 2; // 装箱 调用了 Integer.valueOf(2)
int y = x; // 拆箱 调用了 X.intValue()
自动装箱和拆箱使开发人员可以编写更简洁的代码,使其更易于阅读
示例1:自动装箱
List<Integer> list = new ArrayList<>();
for (int i = 1; i < 50; i += 2)list.add(i);
为什么尽管此处将int值作为原始类型(而不是Integer对象)添加到list中,但代码仍然可以编译?
- 由于List是Integer对象的列表,而不是int值的列表。
- 所以它从i创建一个Integer对象,并将该对象添加到list,因此编译器在运行时将前面的代码转换为以下代码:
List<Integer> list = new ArrayList<>();
for (int i = 1; i < 50; i += 2)list.add(Integer.valueOf(i));
将原始值(例如int) 转换为相应包装类(Integer)的对象称为自动装箱。
总结
当原始值是以下情况时,Java编译器会应用自动装箱:
- 作为参数传递给需要相应包装类方法的对象方法时。
- 分配给相应包装类的变量时。
示例二:自动拆箱
将包装类型Integer
的对象转换为其相应的原始 int
值称为拆箱。当包装类的对象为以下情况时,Java 编译器将应用拆箱:
- 作为参数传递给需要相应基元类型值的方法。
- 分配给相应原始类型的变量。
import java.util.ArrayList;
import java.util.List;public class Unboxing {public static void main(String[] args) {Integer i = new Integer(-8);// 1. Unboxing through method invocationint absVal = absoluteValue(i);System.out.println("absolute value of " + i + " = " + absVal);List<Double> ld = new ArrayList<>();ld.add(3.1416); // Π is autoboxed through method invocation.// 2. Unboxing through assignmentdouble pi = ld.get(0);System.out.println("pi = " + pi);}public static int absoluteValue(int i) {return (i < 0) ? -i : i;}
}
输出如下:
absolute value of -8 = 8
pi = 3.1416
包装类到底有什么作用?
Wrapper 类是包含 原始数据类型(int、char、short、byte 等)的类。换句话说,包装类提供了一种将原始数据类型(int、char、short、byte 等)用作对象的方法。这些包装类在 java.util 包下。
自动装箱用于将原始数据类型转换为相应的对象。
为什么我们需要包装类?
- Wrapper 类将原始数据类型转换为对象。如果我们希望修改传递给方法的参数,则对象是必需的(因为原始类型是按值传递的)。
- java.util 包中的类只处理对象,因此包装类 在这种情况下也有帮助。
- Collection 框架中的数据 结构(例如ArrayList 和 Vector )仅存储对象(引用类型)而不存储 原始类型。
- 需要该对象来支持多线程中的同步。
扩展
- 自动装箱和拆箱
String
引入:我们都知道String
类型最初就被设置成为不可变的。
在Java中String类型为什么要设置成不可变呢?
在Java中,String
对象时不可变的,它们可以在多线程环境中安全地共享,而不需要额外的同步措施。如果String
是可变的,那么在多线程环境中对同一字符串的修改可能会导致竞态条件或不一致行为。
竞态条件或不一致行为 = 在多线程环境中,多个线程同时试图修改同一个可变的字符串 竞态条件或不一致行为 = 在多线程环境中,多个线程同时试图修改同一个可变的字符串 竞态条件或不一致行为=在多线程环境中,多个线程同时试图修改同一个可变的字符串
- 线程安全性:由于
String
对象是不可变的,它们可以在多线程环境中安全的共享,而不需要额外的同步措施。如果String
是可变的,那么在多线程环境中对同一字符串的修改可能会导致竞态条件或不一致的行为。 - 缓存HashCode:
String
类被广泛用作HashMap的key。由于String
是不可变的,它们的哈希码(HashCode)在创建时就可以确定,并且在整个生命周期内保持不变,这样就可以提高HashMap在搜索和插入操作时的性能。 - 安全性:不可变的字符串可以确保其值不会在不应该改变的情况下被修改。这在安全性敏感的场景下是非常重要的,比如在网络通信、加密等方面。
- 性能优化:由于字符串是不可变的,可以进行一些优化举措,比如字符串的常量池(String Pool)可以缓存常用的字符串,避免重复创建相同的字符串对象,从而节省内存和和提高性能。
- 简化设计:不可变性,简化了字符串的设计和使用,使得代码更加清晰和易于理解。
综上所述:将String
设计成不可变的类型有助于提高程序的性能、安全性和可维护性。
概览
String 被声明为 final
,因此它不可被继承。(Integer 等包装类也不能被继承)
在 Java 8 中,String 内部使用 char 数组存储数据。
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];
}
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final byte[] value;/** The identifier of the encoding used to encode the bytes in {@code value}. */private final byte coder;
}
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
不可变的好处
1. 可以缓存 hash 值
因为 String 的 hash 值
经常被使用,例如 :
String 用做 HashMap
的 key
。不可变的特性可以使得 hash 值
也不可变,因此只需要进行一次计算。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如:
在作为网络连接参数
的情况下:
- 如果 String 是可变的,那么在网络连接过程中,String 被改变
- 改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
4. 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
Program Creek : Why String is immutable in Java?
String,Buffer,Builder
1. 可变性
- String 不可变
- StringBuffer 和 StringBuilder 可变
2. 线程安全
- String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
StackOverflow : String, StringBuffer, and StringBuilder
String Pool
字符串常量池(String Pool)
保存着所有字符串字面量(literal strings)。
这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern()
方法在运行过程将字符串添加到 String Pool 中。
当一个字符串调用 intern()
方法时:
- 如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;
- 否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
下面示例中
- s1 和 s2 采用 new String() 的方式新建了两个不同字符串
- 而 s3 和 s4 是通过 s1.intern() 和 s2.intern() 方法取得同一个字符串引用。
- intern() 首先把 “aaa” 放到 String Pool 中,然后返回这个字符串引用,因此 s3 和 s4 引用的是同一个字符串。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true
如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true
关于String中的Intern()源码(jdk8)
/**
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
*/
public native String intern();
扩展
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError
错误。
- StackOverflow : What is String interning?
- 深入解析 String#intern
new String(“abc”)
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。
- “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
- 而使用 new 的方式会在堆中创建一个字符串对象。
创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。
public class NewStringTest {public static void main(String[] args) {String s = new String("abc");}
}
使用 javap -verbose 进行反编译,得到以下内容:
// ...
Constant pool:
// ...#2 = Class #18 // java/lang/String#3 = String #19 // abc
// ...#18 = Utf8 java/lang/String#19 = Utf8 abc
// ...public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=2, args_size=10: new #2 // class java/lang/String3: dup4: ldc #3 // String abc6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V9: astore_1
// ...
在 Constant Pool
中:
- #19 存储这字符串字面量 “abc”
- #3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。
- 在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
public String(String original) {this.value = original.value;this.hash = original.hash;
}
缓存池
引入
想象以下,你有一个池子,里面装满了水。当你需要水时,你不必每次都去打开水龙头,而是直接从池子里面取水,这样不就可以节省时间和资源吗?
在编程中,缓存池通常用于存储一些频繁使用的对象,例如数据库连接、线程、图片资源等。当程序需要使用这些对象时,它可以从缓存池中获取,而不是每次都创建一个新的对象。这样可以避免频繁的创建和销毁对象,提高了程序的性能和相应速度。
什么是缓存池?
缓存池是一个用于存储和重复利用对象的容器,通常用于减少对象的创建和销毁次数,从而提高系统的性能和效率。
new Integer(123)
与Integer.valueOf(123)
的区别在于?
- new Integer(123) 每次都会
新建一个对象
; - Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
//每次新建对象
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false//取得同一个对象的引用
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
valueOf()
方法的实现比较简单
- 就是先判断值是否在缓存池中
- 如果在的话就直接返回缓存池的内容
- 如果不在就直接创建新的对象
论证如果不在线程池范围,就创建新的对象
Integer x = Integer.valueOf("123");
Integer y = Integer.valueOf("123");
//true:论证了观点是从缓存池中的同一个引用
System.out.println(x == y);//127是int类型的边界值,用128来测试
Integer x1 = Integer.valueOf("128");
Integer y1 = Integer.valueOf("128");
//false:说明了是超过缓存池的保存范围就重新创建对象
System.out.println(x1 == y1);
源码:论证如果在的话,就直接返回缓存池的内容。
public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);
}
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
static final int low = -128;
static final int high;
static final Integer cache[];static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUEh = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// range [-128, 127] must be interned (JLS7 5.1.7)assert IntegerCache.high >= 127;
}
编译器会在自动装箱过程调用 valueOf()
方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。
在 jdk 1.8
所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache
很特殊,这个缓冲池的下界是 - 128,上界默认是 127。
但是这个上界是可调的,在启动 jvm
的时候,通过 -XX:AutoBoxCacheMax=<size>
来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high
系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123
你觉得设计缓存池最需要考虑的是什么?
缓存池的设计主要需要考虑到哪几个方面?
- 对象的生命周期管理:需要确保缓存中的对象在不再需要时能够被正确释放,以免造成内存泄露和资源浪费。
- 线程安全性:如果多个线程同时访问缓存池,需要考虑如何保证对象的安全获取和释放,以避免竞态条件和数据不一致性。
- 缓存淘汰策略:当缓存池达到一定大小限制时,需要考虑如何决定哪些对象应该被淘汰除去,以便为了新的对象腾出空间。