夺命连环问——Java基础篇之语法
JDK和JRE有什么区别?
答:
JDK是开发工具包,包含了Java运行时环境JRE和Java其他开发工具。
拓展:
JDK、JRE和JVM的关系:
JDK(Java Development Kit)是Java开发工具包的缩写,包含了Java编译器、Java运行时环境(JRE)和其他开发工具。JDK是开发Java应用程序的必备工具,它提供了编写、编译、调试和运行Java程序所需的所有组件。
JRE(Java Runtime Environment)是Java运行时环境的缩写,包含了Java虚拟机(JVM)和Java类库。JRE提供了Java程序运行的环境,包括了Java虚拟机和Java类库,可以让Java程序在任何支持Java虚拟机的操作系统上运行。
JVM(Java Virtual Machine)是Java虚拟机的缩写,是Java程序执行的核心组件。JVM是一个虚拟计算机,它可以在任何支持Java虚拟机的操作系统上执行Java程序。JVM负责将Java源代码编译成字节码,并在运行时解释执行字节码。
因此,可以简单地将JDK看作是开发工具包,JRE看作是Java程序运行的环境,而JVM则是Java程序执行的核心组件。在实际使用中,通常需要安装JDK来开发Java应用程序,然后通过命令行或集成开发环境(IDE)来运行JRE来测试和调试Java程序,最终通过JVM来执行Java程序。
基本数据类型和引用数据类型有哪些?
答:
Primitive Data Type包括整数、浮点、字符、布尔。
Reference Data Type包括类、接口、数组。
基本数据类型存储数据的值,引用数据类型存储对象的引用。
java的基本数据类型
类型 | 占用字节 | 取值范围 | 包装类 | 默认值 |
---|---|---|---|---|
byte(字节型) | 1 | -128~127(-2的7次方到2的7次方-1) | Byte | 0 |
short(短整型) | 2 | -32768~32767(-2的15次方到2的15次方-1) | Short | 0 |
int(整型) | 4 | -2147483648~2147483647(-2的31次方到2的31次方-1) | Integer | 0 |
long(长整型) | 8 | -9223372036854774808~9223372036854774807(-2的63次方到2的63次方-1) | Long | 0L |
float(浮点型) | 4 | 3.402823e+38~1.401298e-45(e+38 表示乘以10的38次方,而e-45 表示乘以10的负45次方 | Float | 0.0f |
double(双精度浮点型) | 8 | 1.797693e+308~4.9000000e-324(e+38 表示乘以10的38次方,而e-45 表示乘以10的负45次方 | Double | 0.0d |
boolean(布尔型) | 2 | true false | Boolean | false |
char(字符型) | 1 | 汉字字母都可以 | Character | \u0000 |
基本数据类型(Primitive Data Types)包括:
- 整数类型:byte、short、int、long
- 浮点类型:float、double
- 字符类型:char
- 布尔类型:boolean
引用数据类型(Reference Data Types)包括:
- 类(Class)
- 接口(Interface)
- 数组(Array)
基本数据类型是直接存储数据的值,它们的值直接存储在变量中。而引用数据类型是存储对象的引用(内存地址),实际数据存储在堆内存中,变量中存储的是对象的引用。
需要注意的是,基本数据类型具有固定的内存大小,可以直接访问和操作数据,而引用数据类型的大小取决于所引用的对象的大小,需要通过引用来间接访问和操作对象的成员。
Java还提供了自动装箱(Autoboxing)和自动拆箱(Unboxing)的功能,使得基本数据类型和对应的包装类之间可以相互转换,进一步增加了基本数据类型和引用数据类型之间的方便性和灵活性。
注:64 位 JWM 中,int 的长度是多数?
32位 2^32
什么是包装类,包装类的本质是什么?为什么转为包装类?
答:
包装类(Wrapper Class)是为了将基本数据类型转换为对象而引入的一种机制。每种基本数据类型都有对应的包装类,例如Integer对应int,Double对应double等。
Wrapper Class的本质是一个类,它提供了一些方法和属性,来操作对应的基本数据类型的值。
包装类可以将基本数据类型的值包装成对象,从而拥有对象的特性,可以用来调用方法以及进行类型转换,还可以进行更复杂的操作和计算。
知道栈和堆的区别吗?里面分别存的什么数据?
答:
栈是一种后进先出的数据结构,通常存储方法调用、局部变量、以及操作数栈(存储基本数据类型值)的数据等数据。像基本数据类型、对象引用、这些都是栈上的数据。
而且栈的大小由虚拟机决定。当栈的空间不足时,会抛出栈溢出异常(StackOverflowError)。
堆是一种先进先出的数据结构,通常存储对象、数组、静态变量以及类的常量池中的内容。堆是动态分配和回收的内存区域,由Java的垃圾回收机制进行管理。
new String(“abc”)到底创建了几个对象?
答:
new关键字在程序运行的时候,首先会根据已经加载的系统类String,在堆里面去实例化一个字符串对象,然后在这个String的构造方法里面传递一个“abc”字符串,因为Sting里面的字符串是被final修饰的,所以它是一个字符串常量。接下来JVM会去从字符串常量池里面去试图找到它对应的一个对象引用,如果字符串常量池里面没有,就会在堆内存去创建一个“abc”的String对象,并把对象引用保存到字符串常量池里面,后续如果再有字面量“abc”的定义,就直接取,不用在定义。
因此对于这个问题我认为答案有两个:
1、如果’abc‘这个字符串常量不存在,则创建两个对象,分别是’abc‘这个字符串常量,以及’new String‘这个实例对象。
2、如果’abc‘这个字符串常量存在,则只会创建一个对象。
请说说对象的创建过程?
1、类加载检查
首先在实例化对象的时候,JVM会去检查目标对象是否已经被加载并初始化,如果没有,JVM需要去立刻加载目标类,然后去调用目标类的构造器去完成初始化。
目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面,然后是初始化的过程。这个步骤主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。当目标类被初始化以后,就可以从常量池里面去找到对应的类元信息了,并且目标对象的大小,在类加载完成之后呢就已经确定了。
2、分配内存空间
这个时候,就需要去为新创建的对象根据目标对象的大小在堆内存里面去分配内存空间。内存分配的方式呢一般有两种:
第一种是指针碰撞。
第二种是空闲列表。
JVM会去根据Java堆内存是否规整来决定内存的分配方法。
3、初始化“零值”
接下来JVM会去把目标对象里面的普通成员变量初始化为0值,比如说int类型初始化为0,String类型初始化为null。这一操作主要是保证对象里面的实例字段不用初始化就可以直接使用,也就是程序能够直接获取这些字段对应的数据类型的0值。
4、设置对象头
然后JVM还需要对目标对象的对象头做一些设置。比如对象所属的类元信息,对象的GC分代年龄、hashcode、锁标记等等。完成这些步骤以后对于JVM来说,新对象的创建工作已经完成了。
5、执行方法
但是对于Java语言来说,对象创建才算刚刚开始,接下来要做的就是执行目标对象内部生成的init方法,初始化成员变量的值,执行构造块,最后调用目标对象的构造方法去完成对象的创建。
其中init方法是Java文件编译之后在字节码文件里面去生成的,它是一个实例构造器,这构造器里面会把构造块变量初始化调用父类构造器等这样一些操作组织在一起,所以调用init方法能够去完成一系列的初始化动作。
==和equals的区别是什么?
答:
==既可以比较基本类型,也可以比较引用类型,对于基本类型就是比较值,对于引用类型就是比较内存的地址。
equals属于java.lang.Object类里面的方法。 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以我们会误认为 equals是比较值是否相等。
==可以进行值比较,也可以进行引用比较。
equals默认情况是引用比较,重写后变成值比较。
拓展:
/**
* ==和equals的区别
*/
public class Test_01 {
public static void main(String[] args) {
/**
* 比较基本数据类型
*/
int a=1;
double b=1.0;
char c=1;
System.out.println(a==b); //true
System.out.println(a==c); //true
/**
* 引用数据类型
*/
Customer c1 = new Customer("小明", 20);
Customer c2 = new Customer("小明", 20);
System.out.println(c1 == c2); //false
System.out.println(c1.equals(c2)); //false
// String
String str1 = new String("sth");
String str2 = new String("sth");
System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // true
// java中String new和直接赋值的区别
// 对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,
// 如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
String s1="123"; //在常量池中
String s2="123";
System.out.println(s1==s2); //true
String s3 = new String("123"); //存储在堆内存中
System.out.println(s1==s3);
/**
* Integer
*/
Integer i1=1;
Integer i2=1;
System.out.println(i1==i2); //true
Integer i3=128;
Integer i4=128;
System.out.println(i3==i4); //false
System.out.println(i3.equals(i4));
}
}
class Customer {
private String namg;
private int age;
public Customer(){
}
public Customer(String namg, int age) {
this.namg = namg;
this.age = age;
}
public String getNamg() {
return namg;
}
public void setNamg(String namg) {
this.namg = namg;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
为什么同时重写equals和HashCode?怎么重写?
答:
默认情况下,equals()方法会比较对象的内存地址,而hashCode()方法则会根据对象的属性值计算出一个整数值作为哈希码。
如果两个对象通过 equals() 方法比较相等,此时两个对象指向的是同一个内存地址,那么它们的 hashCode() 的值一定是相同的。如果只重写了 equals() 而没有重写 hashCode(),此时equals()两个对象的值是相等的,但hashCode()可能返回不同的哈希码,这将违反一致性的原则。
因此,为了保证对象在哈希表中的正确性和性能,我们需要重写hashCode方法,确保每个对象都有唯一的哈希码。通常的做法是将每个属性的值取反后再进行求和,这样可以避免哈希冲突的问题。
equals和HashCode有什么关系?
首先Java里面任何一个对象都有一个native的hashCode()方法,其次这个方法在散列集合中会用到,比如HashTable、HashMap这些,当往这些集合类去添加元素的时候,需要判断元素是否存在,而直接使用equals的话效率太低,所以一般是直接使用对象的hashCode的值进行取模运算。
如果table里面没有这个对象的hashcode对应的值,那么它就可以把这个对象直接存进去,不用再进行任何的比较,而如果存在的话,就需要调用它的equals方法与新元素进行比较,相同的话呢就直接覆盖,不相同就散列到其他的地址,所以这里存在一个冲突解决的问题,这样实际调用equals方法的次数就大概降低了。
hashcode的值默认是JVM使用随机数来生成的,两个不同的对象可能生成的HashCode会相同,这种情况在Hash表里面体现的就是所谓的Hash冲突,通常会使用链表或者线性探测的方式去解决这个冲突的问题。但是如果两个相同的对象,也就是内存地址指向同一个,那么他们的hashCode一定是相同的。
在理论情况下:
x.equals(y)==true。如果没有重写equals方法,那么对称的内存地址一定是同一个,意味着hashCode必然是相等的。但是如果只重写了equals方法,就有可能导致hashCode不相同,一旦出现这样一个情况就会导致这个类无法和所有的集合类一起工作。所以在实际开发中约定俗成的一个规则,重写equals方法的同时,也需要重写hashCode方法。
equals代码
int、Integer、object 有什么区别?
- int是Java的原始数据类型,表示整数,不能为null。
- Integer是int类型的包装类,用于将int类型包装成对象,可以为null,可以参与面向对象的操作。
- Object是Java的根类,是所有类的父类,可以表示任何其他类型的对象。
异步和同步区别什么?
答:
说白了同步就是要排队,一个等一个(运行时阻塞)。
异步就是不用排队,一个线程在执行的时候,不用继续等,继续执行下一个,上一个线程执行的结果通过回调函数或事件的方式返回。(运行时非阻塞)
异步调用和同步调用都是指程序中不同组件之间的调用方式。
在同步调用中,**程序的某个组件会在调用另一个组件时等待该组件完成后,才会继续执行下一步操作。**也就是说,在同步调用过程中,程序的运行会阻塞等待被调用组件的响应结果。
而在异步调用中,程序的调用不会等待被调用组件返回结果,而是继续执行下一步操作。被调用组件会通过回调函数或事件通知的方式返回结果,程序在收到结果后再对其进行处理。也就是说,在异步调用过程中,程序的运行不会被阻塞,可以同时执行多个调用操作。
异步调用通常用于网络请求、GUI编程和其他需要同时处理多个任务的场景中,而同步调用通常用于需要依次处理任务的场景中。
线程和进程区别是什么?
答:
一个进程可以包含多个线程,这些线程共享同一个进程的内存空间和系统资源。在Java中,一个进程可以包含多个线程,这些线程可以通过继承Thread类或实现Runnable接口来创建。
什么是泛型?使用泛型有哪些好处?什么时候用泛型?泛型需要注意哪些细节?
答:
泛型的本质是参数化类型,可以在编译阶段约束并检查数据类型。
使用泛型可以避免类型转换和重复代码,提高代码的可读性和可维护性。当然最大的好处是在编译期间实现类型安全检查,减少运行时出现的ClassCastException。
在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型,如果能知道是哪个继承体系中的,还可以使用泛型的通配符。
有几点细节需要注意:
Java中的泛型是伪泛型,只在编译阶段有效。
如果不写泛型,类型默认是Object。
泛型中不能写基本数据类型。
数组不支持泛型。
因为Java中的泛型是在编译器进行类型检查和类型擦除的过程中实现的。而数组在创建时需要明确指定元素的类型。例如,如果允许创建泛型数组List<String>[] array = new List<String>[10],那么可以通过数组的元素赋值将不同类型的列表存储在同一个数组中,违背了泛型的类型安全性。
为了解决这个问题,可以使用集合类(如ArrayList、LinkedList等)来代替数组,并通过泛型来实现类型安全的操作。集合类在内部会进行类型检查,可以动态地添加、删除和操作元素,并且支持泛型。
拓展:
1、什么是泛型?
泛型是JDK5后引入的特性,本质是参数化类型。可以在编译阶段约束并检查数据类型。
2、泛型有哪些好处?
- 类型安全
- 提高代码可读性和可维护性
- 避免类型转换和重复代码
- 允许设计者在实现时限制类型
3、哪里定义泛型?
-
泛型类:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类。但要注意,静态方法上的泛型需要在静态方法上声明,不能直接使用。
-
泛型方法:当方法中形参类型不确定时使用。但要注意,在方法申明上定义的泛型只有本方法能用。类后面定义的所有方法都能用。
-
泛型方法:当一个类型未确定的类实现接口时使用。
使用方式有两种:
- 实现类给出具体类型。
- 实现类延续泛型,创建对象时再确定。
4、泛型的继承和通配符
- 泛型不具备继承性,但是数据具备继承性
- 泛型的通配符
- ?也表示不确定的类型,他可以进行类型的限定
- ?extends E:表示可以传递E或者E所有的子类类型
- ?super E:表示可以传递E或者E所有的父类类型
5、泛型的细节?
- 泛型中不能写基本数据类型
- 如果不写泛型,类型默认是Object
- 指定泛型的具体类型后,传递数据时,可以传入该类型和他的子类类型
- 数组不支持泛型
- Java中的泛型是伪泛型,只在编译阶段有效
6、有哪些应用场景?
- 定义类、方法、接口的时候,如果类型不确定,就可以定义泛型
- 如果类型不确定,但能知道是哪个继承体系中的,可以使用泛型的通配符。
为什么数组不支持泛型?
答:
Java中的泛型是在编译器进行类型检查和类型擦除的过程中实现的。而数组在创建时需要明确指定元素的类型。由于泛型在编译后会进行类型擦除,即泛型类型信息会被擦除为其上界或Object类型,因此无法在运行时获取泛型的具体类型信息。
如果数组支持泛型,可能会导致类型安全性问题。例如,如果允许创建泛型数组List<String>[] array = new List<String>[10],那么可以通过数组的元素赋值将不同类型的列表存储在同一个数组中,违背了泛型的类型安全性。
为了解决这个问题,可以使用集合类(如ArrayList、LinkedList等)来代替数组,并通过泛型来实现类型安全的操作。集合类在内部会进行类型检查,可以动态地添加、删除和操作元素,并且支持泛型。
感兴趣的伙伴可以参考文章——(7条消息) 面试题——深入理解Java泛型机制_如果我是枫的博客-CSDN博客
Java和C++的泛型有什么区别?
- C++模板可以使用int等基本数据类型。Java则不行,必须转而使用Integer
- Java中,可以将模板的类型参数限定为某种特定类型。例如,你可能会使用泛型实现CardDeck,并规定参数必须扩展自CardGame。
- C++中,类型参数可以实例化,Java不可以实例化
- Java中,类型参数(即MyClass中的Foo)不能用于静态方法和变量,因为他们会被MyClass和MyClass共享。但在C++中,这些类是不同的,类型参数可以用于静态方法和静态变量。
- 在Java中,不管类型参数是什么,MyClass的所有实例都是同一类型。类型参数会在运行时被抹去。而C++中,参数类型不同,实例类型也不同
深拷贝和浅拷贝有什么区别?
答:
浅拷贝是只复制对象本身的值,不复制它所引用的对象,新旧对象共享一个引用对象。可以通过实现Cloneable接口和覆盖clone()方法来实现浅拷贝。
深拷贝是复制对象本身和所有它所引用的对象,新旧对象不共享任何引用对象。可以通过实现 Serializable 接口和使用序列化/反序列化来实现深拷贝。
JAVA反射机制提供了什么功能?怎么实现?
答:
1.动态获取类的信息
2.动态创建对象
3.动态调用方法
4.修改私有属性和方法的访问权限
5.实现注解处理器。
在使用Java的反射功能时,基本首先都要获取类的Class对象,再通过Class对象获取其他的对象。
实现注解处理器:反射机制可以用于处理注解,可以通过反射获取类、方法、字段上的注解信息,并根据注解进行相应的处理逻辑。
具体实现反射的功能需要使用Java中的java.lang.reflect
包中的相关类和方法。下面是一些常见的反射操作示例:
- 获取类的信息:
javaCopy codeClass<?> clazz = MyClass.class; // 获取类的Class对象
String className = clazz.getName(); // 获取类名
Constructor<?>[] constructors = clazz.getConstructors(); // 获取构造方法
Field[] fields = clazz.getDeclaredFields(); // 获取所有字段
Method[] methods = clazz.getDeclaredMethods(); // 获取所有方法
Annotation[] annotations = clazz.getAnnotations(); // 获取所有注解
- 创建对象:
javaCopy codeClass<?> clazz = MyClass.class;
Constructor<?> constructor = clazz.getConstructor(); // 获取无参构造方法
Object obj = constructor.newInstance(); // 创建对象
- 调用方法:
javaCopy codeClass<?> clazz = MyClass.class;
Method method = clazz.getMethod("methodName", parameterTypes); // 获取方法
Object result = method.invoke(obj, arguments); // 调用方法
- 访问和修改私有字段:
javaCopy codeClass<?> clazz = MyClass.class;
Field field = clazz.getDeclaredField("fieldName"); // 获取字段
field.setAccessible(true); // 设置访问权限
Object value = field.get(obj); // 获取字段值
field.set(obj, newValue); // 设置字段值
通过这些反射操作,可以在运行时动态地获取和操作类的信息、创建对象、调用方法以及访问和修改字段。需要注意的是,反射操作可能会涉及到异常处理和安全性考虑,需要适当地处理异常和权限问题。
Java反射有什么优缺点?
答:
Java中反射的优点有几个。
第一个是增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作。
第二个是可以提高代码的复用率,比如动态代理,就是用到了反射来实现的。
第三个呢是可以在运行是轻松获取任意一个类的方法、属性,并且还能通过反射动态调用。
反射的缺点也有几个。
第一个是反射会涉及到动态类型的解析,所以JVM无法对这些代码进行优化,导致性能要比非反射调用更低。
第二是使用反射之后代码的可读性会下降。
第三个是反射可以绕过一些限制访问的属性或者方法,可能会导致破坏了本身的抽象性,而且会造成一些安全性问题。
以上就是我的理解
简单说一下你对序列化和反序列化的理解?
关于这个问题我需要从几个方面来回答。
首先我认为之所以需要序列化,它的核心目的是为了解决网络通信之间的一个对象传输的一个问题。也就是说怎么样去把当前JVM进程里面的一个对象跨网络传输到另外一个JVM进程里面进行恢复。而序列化呢就是把内存里面的对象转化为字节流,以便用来实现存储和传输,而反序列化就是根据从文件或者网络上获取到的对象的一个字节流,根据字节流里面保存的对象描述信息和状态,重新构建一个新的对象。
其次序列化的前提是为了去保证通信双方对于对象的一个可识别性,所以很多时候我们会把对象先转化为通用的解析格式,比如说Json、Xml等,然后再把它们转化为数据流进行网络传输,从而去实现跨平台或者跨语言的这样一个可识别性。
最后再补充一个序列化的选择问题,市面上开源的序列化技术非常多,比如说像Json、Xml、Protobuf、Kyro、haison等等。在实际应用中那种更合适我认为有几个关键因素。
JDK动态代理为什么只能代理有接口的类?
我认为这个问题的核心本质是JDK动态代理本身的机制决定的。
首先呢在Java里面动态代理是通过Proxy.newProxyInstance()来实现的,他需要传入被动态代理的一个接口类,之所以要传入接口而不能传入类,还是取决于JDK动态代理的一个底层实现。
JDK动态代理会在程序的运行期间去动态生成一个代理类叫$Proxy0,那么这个动态生成的代理类呢会去继承一个java.lang.reflect.Proxy这样一个类,同时还会去实现被代理类的接口。在Java里面是不支持多种继承的,而每个动态代理类都继承了一个Proxy,所以就导致JDK里面的动态代理只能代理接口,而不能代理实现类。
如果要去针对普通类来去做动态代理,可以选择cglib组件,它会动态生成一个被代理类的子类,子类重写父类的所有非final修饰的方法,在子类中去拦截父类的所有方法的一个调用,从而去实现动态代理。
常见的运行时异常有哪些?
答:
ArithmeticException(算术异常)
ClassCastException(类转换异常)
IllegalArgumentException(非法参数异常)
IndexOutOfBoundsException(下标越界异常)
NullPointerException(空指针异常)
SecurityException(安全异常)
常见的非运行时异常有哪些?
答:
IOException:输入输出操作异常
SQLException:数据库操作异常
ClassNotFoundException:找不到指定的类异常
NoSuchMethodException:找不到指定方法异常
NoSuchFieldException:找不到指定字段异常
IllegalAccessException:非法访问异常