1、JDK、JRE、JVM三者之间的关系?
JDK:JAVA Development Kit(它是Java开发运行环境,在程序员的电脑上当然必要安装JDK) 不仅提供了Java程序运行所需的JRE,还提供了一系列的编译,运行等工具,如javac.exe,jar.exe等。
JRE: JAVA Runtime Environment(它是Java运行环境,如果你不需要开发,只需要运行Java程序【不是绝对的,比如使用JSP部署Web应用程序还是要JDK的】,那么你可以安装JRE)包含了java虚拟机,java基础类库。
JVM:JAVA Virtual Machine(JAVA虚拟机)是运行Java字节码的虚拟机。字节码和不同系统的JVM是实现Java语言”一次编译,到处运行"的关键。
三者的包含关系如下图:
JDK = JRE + 开发工具集
JRE = JVM + JAVA的核心类库
2、面向对象和⾯向过程的区别
面向对象:面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性,但是面向对象性能比面向过程低。
面向过程:面向过程性能比面向对象高,类需要实例化,开辟空间,耗资源。但是面向过程不易维护、易复用、易扩展。
3、Java和C++的区别
- 都是面向对象的语言,都支持封装、继承和多态
- Java不支持指针访问内存,程序更安全。
- java是单继承(接口可以多继承),C++支持多继承
- Java有自动内存管理机制,不需要程序猿手动释放内存
- C语言中的字符串最后有一个额外的 '\0' 来表示结束,Java语言没有结束符。
4、对Java的加载与执行的理解
问题拆解:Java程序非常重要的哪两个阶段?
编译阶段和运行阶段。
编译期间:
Java程序员直接编写的Java代码(源程序)是无法执行,无法被JVM识别的。必须得经历一个编译:将这个 "普通文本" 转为 "字节码" (JVM能识别的"字节码" )
普通文本转为字节码的过程是编译。xxx.java源代码符合语法规则就会编译通过,形成xxx.class文件
注意:字节码不是二进制文件,如果是二进制文件,那还要JVM干嘛。
运行期间:
JRE起作用,JVM将.class字节码文件装载进去,对字节码进行解释(解释器逐行执行),转为二进制。后面JVM将生成的二进制码交给OS操作系统执行二进制码,操作系统就会执行二进制码和硬件进行交互。
在windows上可以运行,其他系统也可以运行:如果是在linux上运行,只要把生成的.class文件拷贝过去。在这个过程(转二进制到与硬件交互),没有.java文件任何事,即使现在删除也没关系(最好不要,你还会修改代码的呢),对字节码没有影响。
在这使用CMD编写代码过程:
javac 命令负责编译(JDK的bin目录下有javac.exe),java 命令负责运行 (JDK的bin目录下有java.exe)
5、Java变量分为哪几类
在Java中所有的变量分为:成员变量 和 局部变量
先了解它们的区别:成员变量是指这个类的变量,局部变量是类中的方法内定义的变量
成员变量包括:实例变量 和 类变量
实例变量是没有static修改的变量,类变量是指以static修饰的。
实例变量 和 类变量的区别:
1)访问:实例变量是通过定义类的对象访问,类变量可以通过类或者类的对象访问。
2)生命周期:实例变量与类的对象生命周期共存亡,类变量与类共存亡。
3)位置:实例变量存放在堆中,类变量存放在方法区。
局部变量包括:形参 和 方法局部变量(在方法内定义)存放位置:栈中。
局部变量与成员变量的区别?
1、定义位置不同:
局部变量:在方法的内部
成员变量:在方法的外面,直接写在类当中
2、作用域不同:
局部变量:只有在方法中才能使用,出了方法就不能再用了
成员变量:整个类全部可以用
3、默认值不一样
局部变量:没有默认值,如果使用必须手动进行赋值
成员变量:如果没有赋值会有一个起始值,规则和数组一样
6、&& 与 & 的区别是?|| 和 | 的区别是?
首先这个两者&&和&运算结果没什么区别,完全相同,只不过 && 会发生"短路"的现象:比如:Flag1 && Flag2,当Flag1为false是Flag2就不会判断。
|| 和 |也存在上述问题:如果第一个操作数是true,||运算符就返回true,无需考虑第二个操作数的值。
但&和|却不是这样,它们总是要计算两个操作数。性能效率就不如上面的两个。
重要区别简洁描述:条件布尔运算符性能比较好。它检查第一个操作数的值,再根据该操作数的值进行操作,可能根本就不处理第二个操作数。
7、++i和i++的小问题
++无论出现在变量前还是后,只要++运算结束,变量一定会自加1.
int i = 10;
i++;
System.out.println(i); // 11
int k = 10;
++k;
System.out.println(k); // 11
i++:是先使用变量的值,在执行++的操作,++i:是先执行++操作,在使用变量的值。
解开以上题目的窍门是什么?拆分代码过程:
// ++ 在变量的后面 如 i++
int i = 10;
int a = i++; // 拆分代码 int a = i; i= i++;
System.out.println(i); // 11
System.out.println(a); // 10
// ++ 在变量前面 ++i
int b = 10;
int c = ++b; // 拆分代码 b = b++; int c = b;
System.out.println(b); // 11
System.out.println(c); // 11
8、基本数据类型的运算问题
首先Java的数值有默认的类型的
整数 :默认为int类型,比如 4 + 1;返回的类型是int类型
带有小数:默认为double类型,比如 1.2 + 1.2;返回的类型是double类型
理解基本数据类型运算关键:"低级别"数据类型 和 "高级别"数据类型运算,得出的结果会自动转向"高级别"数据类型。
byte,short,char 类型混合运算时,先各自转换成 int 类型再做运算。
多说也记不住,先看看案例:
//案例1
char a = 97;
char b = 'B';
b = a + 1; //编译不通过的,需要强转;a + 1 这里的"高级别"类型是int,转为int结果,不可以赋值给char的
// 案例2
byte b1 =1;
byte b2 =2;
b1 =b1 + b2;//编译不通过,需要强转为int类型,byte,short,char 类型混合运算时,先各自转换成 int 类型再做运算
b2 +=b1;//编译通过,这个等同于 b2 = (byte)(b2+b1)
// 案例3
float f1 = 1.2; //不可以通过,因为1.2在Java中默认是double类型
float f2 = 1.2f;//通过
float f3 = 1.2f;//通过
f2 = 1.2+f3; //不可以通过,需要强转为double类型,1.2为double类型
f3=1.2+1.4; // 不可以通过编译,1.2+1.4都是double类型相加,需要强转
总结一下大致规则:
1)第一条:八种基本数据类型中,除 boolean 类型不能转换,剩下七种类型之间都可以进行转换。
2)第二条:如果整数型字面量没有超出 byte,short,char 的取值范围,可以直接将其赋值给byte char 类,short,型的变量;
3)第三条:小容量向大容量转换称为自动类型转换,容量从小到大的排序为:byte < short(char) < int < long < float < double,其中 short和 char 都占用两个字节,但是char 可以表示更大的正整数。
4)第四条:大容量转换成小容量,称为强制类型转换,编写时必须添加“强制类型转换符”,但运行时可能出现精度损失,谨慎使用;
5)第五条:byte,short,char 类型混合运算时,先各自转换成 int 类型再做运算。
6)第六条:多种数据类型混合运算,各自先转换成容量最大的那一种再做运算;
9、⾃动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来。
拆箱:将包装类型转换为基本类型。
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
char | Character |
在老版本的装箱:Integer i =
new
Integer(
10
);后面只需要 Integer i =5;
看一下简单代码:
public class Main {
public static void main(String[] args) {
Integer i =1;//装箱,1是int类型
int n = i; // i是Integer类赋值给int
}
}
装箱过程的debug:
拆箱过程的bebug:
得知自动调用静态方法Integer.valueof(int)装箱,在拆箱是自动调用Integer.intValue方法。不用使用debug模式查看调用方法情况也可以使用反编译。
装箱过程调用包装器的valueof方法实现的,拆箱过程是调用包装器的xxxValue方法(包装类转基本类型:调用xxxValue()方法)
面试问题:
public class Main {
public static void main(String[] args) {
Integer i1 =1;// 底层是去方法去缓存池中找创建好的对象
Integer i2 =1;// 底层是去方法去缓存池中找创建好的对象
Integer i3 =300;// 底层还是 Integer i3 = new Integer(300); i3是引用,保存内存地址执行对象
Integer i4 =300;// 底层还是 Integer i4 = new Integer(300); i4是引用,保存内存地址执行对象
System.out.println(i1==i2);//true
System.out.println(i3==i4);//false
}
}
Java中为了提高程序的执行效率,将byte范围的[-128,127]之间所有的包装对象提前创建好,放到方法区的"缓存池"中,目的是 :只要在这个区间的数据不需要再new。直接从缓存池中取出。
i1和i2变量保存对象的地址是一样,输出true。JVM图如下:
查看源代码分析如下:下面这段代码是Integer的valueOf方法的具体实现
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache类的实现为:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
VM.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
当Integer i1=1; 调用valueOf(1),判断i是否在[-128,127]之间,如果在这范围,去加载内部类IntegerCache,在类加载同时执行static代码块,这时就初始化缓存池,创建256个对象。返回指向缓存池已经存在的对象引用;不在范围中就创建一个新对象(返回)
Integer i2=1;也是和上面一样操作,这时它们会指向同一个对象的地址。
触发自动拆箱、自动装箱:(使用debug模式可以清楚看到过程)
自动装箱:
1)插入基本数据类型到集合中(插入对象)
2)Integer i1 =1
3)基本类型与包装类比较equals();变量转为包装类
自动拆箱:
1)包装类之间进行 +-*/等运算,拆箱为基本类型
2)包装类和基本类型执行==操作。
10、== 与 equals()重要
==:可以使用在基本类型变量和引用类型变量,它是一个运算符。
如果比较的是基本类型变量,比较的是两个变量保存的数据值是否相等(不一定要相同类型)
如果比较的是引用类型变量,比较的是两个变量地址值是否相等(即引用变量是否指向同一个对象实体)
案例代码操练:
public class Main1 {
public static void main(String[] args) {
A a1 = new A("A",20);
A a2 = new A("A",20);
double d = 5.0;
int i = 5;
System.out.println(d==i);// true
System.out.println(a1==a2);//false,地址值不一样,保存的内容相同的
}
}
是不是有疑问?为什么int类型的和double类型比较会相等,其实又回到上面的第8点,运算符操作前先把类型统一转为"高级别"类型,都变成double类型,在进行比较值是否相等。
equals():它属于java.lang.Object类型的方法,作⽤也是判断两个对象是否相等。一般分为两种情况:
- 情况一:类没有重写equals()方法(覆盖Object类的),则通过equals()比较,等价于"=="
- 情况二:类重写了equals()方法,一般重写equals()方法比较对象的内容(类中相应的属性都相等否)是否相等;内容相等就返回true。
public class test {
public static void main(String[] args) {
String a = new String("ab"); // a 为⼀个引⽤
String b = new String("ab"); // b为另⼀个引⽤,对象的内容⼀样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,⾮同⼀对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("a equals b");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
上面的String类中重写了equals()方法,不是比较两个引用对象的地址值,而是比较两个对象的"实体内容"是否相同。其实还有像File、Date、包装类等都重写了equals()方法,比较的都变成了实体内容。通常情况下,我们自定义的类如果使用equals()方法也需要重写equals()方法,这样也就比较对象的属性值是否都相等。
说了这么多,还是看看Object类中的equals()方法原型:
public boolean equals(Object obj) {
return (this == obj);
}
这个Object的equals()和==作用相同的,比较对象的地址值。
11 、hashCode 与 equals(重要)
hashCode()介绍
hashCode()的作用是获取哈希码(散列码)返回一个int类型。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode与equals之间的区别
- hashCode如果相等的情况下,对象的值不一定相等
- 但是equals比较对象的内容相同,那么hashCode一定相等。
如果重写了equals()方法后,一定要重写hashCode()方法为什么
我们要遵循:
hashCoed相等的情况下,对象的值不一定相等
但是equals比较对象的内容相同,那么hashCode一定相等,所有重写了equals()方法,需要重写hashCode保证原则不变。不然重写了equals()方法不重写hashCode方法,两个对象内容相同,但是hashCode不同。
12、在Java中定义一个不干事且没有参数的构造方法的作用
构造方法的作用是为堆区中的对象的属性初始化。
Java程序在执行子类的构造器方法之前,如果没有使用super()调用父类的特定的构造方法,则会默认(不写super()也是默认写了)调用父类中的无参构造方法,这时父类中没有无参构造方法,子类的所有构造方法又没有调用super()来调用父类的其他有参构造方法。子类编译时会报错。除非父类加上无参构造方法或者使用super调用其他有参构造方法(一般使用在子类的有参构造方法中且必须出现在首行)。
既然体到super关键字,就顺便提一下:super的使用:子类重写了父类的方法后,在子类调用父类的方法必须加上super.方法,表明调用父类的方法。不加上super关键字,默认是在子类中找,没找到再去父类中找。
13、数组的初始化方式
第一种:静态初始化:创建和赋值同时进行
int [] a = new int[]{1,2,3,4,5}
//也或者
int str []={1,2,3,4};
第二种:动态初始化:数组定义与数组元素赋值分开进行
int a [] = new int[4];
a[0]=1;
a[1]=2;
a[2]=3;
a[3]=4;
//最常见错误方式
int bb [] = new int[2]{1,2};//错误❌