基础
JVM vs JDK vs JRE
JVM
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
JVM并不是只有一种!只要满足JVM规范,每个公司、组织或者个人都可以开发自己的专属 JVM。 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。
除了我们平时最常用的 HotSpot VM 外,还有 J9 VM、Zing VM、JRockit VM 等 JVM 。维基百科上就有常见 JVM 的对比:Comparison of Java virtual machines ,感兴趣的可以去看看。并且,你可以在 Java SE Specifications 上找到各个版本的 JDK 对应的 JVM 规范。
JDK 和 JRE
JDK是Java Development Kit 缩写,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
JRE是Java运行时环境。它是运行已编译Java程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。
基本数据类型
Java 中的几种基本数据类型了解么?
Java中有8种基本数据类型:
- 数字类型:
- 整数型:
byte、short、int、long - 浮点型:
float、double
- 整数型:
- 字符类型:
char - 布尔型:
boolean
这 8 种基本数据类型的默认值以及所占空间的大小如下:
| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
|---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768 ~ 32767 |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 |
char | 16 | 2 | 'u0000' | 0 ~ 65535 |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean | 1 | false | true、false |
对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
另外,Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一(《Java 编程思想》2.2 节有提到)。
注意:
- Java 里使用
long类型的数据一定要在数值后面加上 L,否则将作为整型解析。 char a = 'h'char :单引号,String a = "hello":双引号。
这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
基本类型和包装类型的区别?
- 成员变量包装类型不赋值就是
null,而基本类型有默认值且不是null。 - 包装类型可用于泛型,而基本类型不可以。
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 - 相比于对象类型, 基本数据类型占用的空间非常小。
为什么说是几乎所有对象实例呢? 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
⚠️ 注意 : 基本数据类型存放在栈中是一个常见的误区! 基本数据类型的成员变量如果没有被 static 修饰的话(不建议这么使用,应该要使用基本数据类型对应的包装类型),就存放在堆中。
装箱和拆箱
Integer a = 100;这是自动装箱(编译器调用的是static Integer valueOf(int i))
把基本类型用它们相应的引用类型包装起来,使其具有对象的性质。int包装成Integer、float包装成Float。
int b = new Integer(100);这是自动拆箱。和装箱相反,将引用类型的对象简化成值类型的数据。
一个Integer与int比较,先将Integer转换成int类型,再做值比较,所以返回的是true。
Integer a = 300;
int b = 300;
System.out.println(a == b); //trueequals
JDK中说明了实现equals()方法应该遵守的约定:
- 自反性:x.equals(x)必须返回true。
- 对称性:x.equals(y)与y.equals(x)的返回值必须相等。
- 传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。
- 一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变。
- 非null:x不是null,y为null,则x.equals(y)必须为false。
Object类中的equals()是用来比较地址的。
public class Object {
public boolean equals(Object obj) {
return (this == obj);
}
}String、Date、Integer对equal()进行了重写。比较内容是否相同。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
}
package java.util;
public class Date
implements java.io.Serializable, Cloneable, Comparable<Date>
{
public boolean equals(Object obj) {
return obj instanceof Date && getTime() == ((Date) obj).getTime();
}
}
package java.lang;
public final class Integer extends Number implements Comparable<Integer> {
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
}- 对于基础数据类型,使用
==比较值是否相等。 - 对于复合数据类型(类):
==比较的是对象在内存中的存放地址(堆内存地址)。equals没有覆盖的,比较的是对象在内存中的存放地址(堆内存地址)。像String、Integer、Date等覆盖了equals()方法的类型,比较的是值是否相同。
Integer a = 300;
Integer b = 300;
System.out.println(a == b); //false 比地址
System.out.println(a.equals(b)); // true 比值
Integer c = 127;
Integer d = 127;
System.out.println(c == d); //true 缓存池
System.out.println(c.equals(d));//true 比值
int e = 300;
int f = 300;
System.out.println(e == f); //true 比值hashcode
public class Object {
public native int hashCode();
}hashCode()是一个native方法,而且返回值类型是整形;实际上,该native方法将对象在内存中的地址作为哈希码返回,可以保证不同对象的返回值不同。
JDK中对hashCode()方法的作用,以及实现时的注意事项做了说明:
- hashCode()在哈希表中起作用,如java.util.HashMap。
- 如果对象在equals()中使用的信息都没有改变,那么hashCode()值始终不变。
- 如果两个对象使用equals()方法判断为相等,则hashCode()方法也应该相等。
- 如果两个对象使用equals()方法判断为不相等,则不要求hashCode()也必须不相等;但是开发人员应该认识到,不相等的对象产生不相同的hashCode可以提高哈希表的性能。
设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码。
对于两个对象:
- 如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
- 如果两个对象的hashcode值相等,则equals方法得到的结果未知。
整型池
-128-127。Cache是IntergerCache内部类的一个静态数组。包装对象在这个范围内,则直接从池中获得对象。
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}Interger内部IntergerCache
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}Integer a = 100;
Integer b = 100;
Integer c = new Integer(100);
System.out.println(a == b); //true
System.out.println(a == c); //false字符串池
JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,即字符串池(String Pool)。字符串池由String类私有的维护。
字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化,在Permanent Generation中专门有一块区域用来存储字符串常量池(一组指针指向Heap中的String对象的内存地址)。
String str1 = "abc";执行这句话时,会先去“字符数据池”搜索时候有“abc”这个字符串,如果有,则将字符串的首地址赋值给str1,如果没有,生成一个新的字符串“abc”并且将首地址赋值给str1。
String str2 = new String("abc");
String str3 = new String("abc");执行这句话时,不会考虑时候已经存在了“abc”这个字符串,而是直接生成一个新的字符串“abc”并将首地址赋值给str2、str3,注意“abc”并不放在“字符数据池”中。
String str4 = str2.intern();
String str5 = "abc";对于上面使用new创建的字符串对象,如果想将这个对象的引用加入到字符串常量池,可以使用intern方法。调用intern后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量。
System.out.println(str1 == str2); //false
System.out.println(str2 == str3); //false
System.out.println(str1 == str4); //true
System.out.println(str2 == str4); //false
System.out.println(str1 == str5); //true操作运算符
+
再使用+的表达式中,只要遇到string字符串,则所有的数据都会转换为string类型进行拼接,如果是原始数据则直接拼接,如果是对象,则调用tostring方法的返回值然后拼接。
在+表达式中,String字符串具有最高优先级。
=
int num = 20;
String str = "java";num是基本类型,值就直接保存在变量中。而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。
对于基本类型num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。
数值类型直接的转换
double->float->long->int
short s1 = 1;
short s2 = 2;
short b3 = s1 + s2; // error如果两个操作数中有一个是double类型的,另一个操作数就会转换为double类型。
否则,如果其中一个操作数是float类型,另一个操作数会转换为float类型。
否则,如果其中一个操作数是long类型,另一个操作数会转换为long类型。
否则,两个操作数都将被转换为int类型。