&& 这是一个关于Java大学“牲”玩家不断提高的文章,会不断更新,希望大家关注+评论+收藏。
目标:帮助一个小小目标的Java同学找到工作就是我的梦想。
这是第一期每期我会做7个题,其实也不用背特别多,我做的都是核心,看完之后拿offer拿到爽。
为什么是 7 题?
因为彩虹是7种颜色。
彩虹被视为带来希望、改变和完美的象征,这些象征意义源自其美丽的外观、出现的条件和历史传统。我希望每个人都能怀抱希望、勇于改变,不断追求自我完善。
希望你们也能像彩虹一样,在生活的旅途中永远保持乐观和对美好的向往。
废话不多说,直接开始。。。
001. 说一下Java的数据类型吧?
Java 的数据类型分为两大类:
1. 基本类型(Primitive Types)
2. 引用类型(Reference Types)。
1. 首先是基本类型(Primitive Types)
Java 中的基本数据类型有八个,分别是:
- boolean: 布尔类型, 取值只有 true 和 false。
- byte:字节类型,占用 1 个字节,范围为 `-128` 到 `127`。
- short:短整型,占用 2 个字节,范围为 `-32768` 到 `32767`。
- int:整型,占用 4 个字节,范围为 `-2147483648` 到 `2147483647`。
- long:长整型,占用 8 个字节,范围为 `-9223372036854775808` `9223372036854775807`。
- float:单精度浮点型,占用 4 个字节,有效位数为 6-7 位。
- double:双精度浮点型,占用 8 个字节,有效位数为 15 位左右。
- char:字符类型,占用 2 个字节,用于表示 Unicode 字符。
博主记忆方式:byte,short,int,long(1248)个字节。
boolean,char,float,double(1248)个字节,依次递增。
2. 然后是引用类型(Reference Types)
引用类型是指从一个类派生的类型,如类、接口和数组等。不同于基础类型,引用类型的变量不是直接存储值,而是存储指向内存地址的引用,实际的值存储在对象中。
常见的引用类型(一般不会太深入,实习/应届简单描述一下即可):
- 类(Class):引用指向一个 Java 类的实例。
- 接口(Interface):引用指向一个实现了某个接口的类的实例。
- 数组(Array):引用指向一个数组对象。
Java 的数据类型是构建Java语言的基础,基础类型在Java中有着广泛的应用,可以作为方法的参数和返回值,用于数组和其他数据结构等。理解Java基础类型和引用类型的差异和用法,是编写Java程序的基础。
3. 紧接着面试官可能会问一下两者的区别:
这里如果面试官没问,那就自己说出来,让 Ta 觉得你和普通学生不一样。
简单回答四个方面即可:1. 存储方式 2. 数据大小 3. 默认值 4. 包装类型
1. 先是存储方式:
- 基本数据类型(Primitive Data Type)的变量直接存储数据的值,它们在内存中占据固定的空间大小,并且直接包含数据值。例如,int 类型占据 4 个字节的内存空间,用于存储整数值。
- 引用数据类型(Reference Data Type)的变量并不直接存储数据的值,而是存储对对象的引用(内存地址),对象的实际数据位于堆内存中。引用数据类型的变量本身只占据固定大小的内存空间,用来存储对象的引用。
2. 然后数据大小:
- 基本数据类型在内存中占据的空间大小是固定的,不会随着数据类型的改变而改变。
- 引用数据类型的对象可以是任意大小,它们的存储空间在堆内存中动态分配,大小取决于对象的属性和方法。
3. 再然后默认值:
- 基本数据类型有默认值,例如,int 类型的默认值是 0,boolean 类型的默认值是 false。
- 引用数据类型的默认值都是 null,即在声明引用数据类型的变量时,如果没有明确赋值,该变量的初始值为 null。
4. 最后是包装类型:
- Java中提供了对基本数据类型的包装(Wrapper)类,例如 Integer、Double、Boolean 等,它们是引用数据类型。这些包装器类可以用于将基本数据类型转换为对象形式。引用数据类型本身就是对象,不需要特殊的包装器类来操作。
随后,面试官可能会问到这个问题,
为什么Java号称是纯粹的面向对象的语言,还是引入了基本数据类型。
Java之所以被称为面向对象的编程语言,是因为它支持面向对象编程的特性,如封装、继承、多态等。Java仍然引入基本数据类型的原因涉及到在编程过程中对数据操作的需求和效率考虑等问题。
一方面,基本数据类型的引入是为了让程序员能够更加高效地处理简单的数值和字符数据。相比于对象封装的数据类型,基本数据类型能够在内存中占用较少空间,并且执行速度更快。这使得基本数据类型对于一些简单的数据操作更为高效和便捷,可以提高程序的性能和执行效率。
另一方面,引入基本数据类型也让Java语言在设计上更加灵活和适用。它保留了对基本数据的直接操作,同时又能够充分支持面向对象编程的特性,使得Java语言在开发过程中能够兼顾高效性和灵活性。
因此,Java引入基本数据类型并不与其面向对象的特性相矛盾,相反,这种设计考虑是为了使Java在处理不同类型数据时更加灵活高效,既保留了传统的数据操作方式,又兼顾了面向对象编程的优势。这也是Java成为一门强大而多用途编程语言的原因之一。
002. 说一下Java的四种引用吧?
这个问题只需要大概说一下每个引用的基本概念和应用场景即可,要是非要深入,那只能说你运气不好,我反正没有遇见需要深入理解情况。
1. 强引用(Strong Reference):
强引用是最常见的引用类型,它是默认的引用类型。当我们使用关键字"new"创建一个对象时,通过一个引用变量来引用它,这个引用就是强引用。即使发生内存不足的情况,垃圾回收器也不会回收强引用的对象。只有在没有强引用指向一个对象时,垃圾回收器才会将其回收。强引用常用于需要长时间引用对象的场景,比如全局对象、单例模式或者任何需要该对象一直存在的情况。
2. 软引用(Soft Reference):
软引用是一种相对强引用弱化的引用类型。使用软引用可以在内存不足时,允许被回收的对象。这样可以有效地避免内存溢出的情况发生。当系统内存不足时,垃圾回收器会根据需要回收软引用对象,但只有当内存真正不足时才会回收。软引用常用于缓存的实现,允许在内存不足时释放一些缓存对象,以保证系统的稳定性和性能。
3. 弱引用(Weak Reference):
弱引用是一种比软引用更弱的引用类型。使用弱引用创建的对象在垃圾回收器工作时,无论内存是否足够,都会被回收。弱引用通常用于简单的缓存场景,当对象不再被强引用引用时,可以快速释放相关资源。弱引用通常用于缓存系统中临时对象的管理,在对象不再被强引用引用时,快速释放相关资源。
4. 虚引用(Phantom Reference):
虚引用是最弱的引用类型,几乎没有实际的直接使用价值。虚引用的主要作用是跟踪对象被回收的活动。当对象被垃圾回收器标记为可回收时,会将其放入一个引用队列中,开发人员可以通过监测该队列,了解对象何时被回收。虚引用通常用于更复杂的资源管理场景,例如内存映射文件的释放,它允许程序在垃圾回收前进行一些必要的清理操作。
应用场景总结:
强引用是最常见最常用的引用类型,用于长期持有对象,例如 new 出来的对象;
软引用用于缓存实现,允许在内存不足时释放缓存对象;
弱引用用于临时对象的管理,在没有强引用指向对象时快速释放资源;
虚引用通常用于复杂资源的跟踪和管理,允许程序在对象回收前进行清理操作。
003.说一下接口和抽象类的区别吧?
这个问题的回答方式:先说各自的概念,然后说区别,如何选择,最后说应用场景。
1. 接口(Interface):
- 接口是一种与实现无关的引用类型,是一组方法声明的集合。
- 接口中只包含方法的声明、常量和默认方法的定义,没有实现的方法。
- 接口用于定义类之间的契约(contract),规定了类应该具备的方法和常量。
- 类可以实现多个接口,通过实现接口可以实现多继承的效果。
- 接口中的方法默认为 public abstract,字段默认为 public static final。
2. 抽象类(Abstract Class):
- 抽象类是一种不能被实例化的类,用于对类的共性特征进行抽象。
- 抽象类可以包含抽象方法(只有声明,没有实现)和已实现的方法。
- 抽象类可以包含成员变量和构造方法。
- 一个类只能继承一个抽象类,无法实现多继承。
- 抽象类可以用作其他类的基类,子类必须实现抽象类中的抽象方法,或者将自己定义为抽象类。
两者的区别
- 实现方式:接口只能包含方法声明,而抽象类可以包含已实现的方法。
- 多继承:类可以实现多个接口,但只能继承一个抽象类。
- 构造方法:抽象类可以包含构造方法,而接口不能包含构造方法。
- 字段:接口中的字段默认为 public, static, final,而抽象类中的字段没有默认修饰符。
- 设计理念:接口用于定义规范和约定,抽象类用于定义类的共性特征。
各自的应用场景如下:
接口的应用场景:
1. 定义类之间的契约(contract): 当需要实现多个类之间的一致性行为或者规范时,可以使用接口。例如,定义 Comparable 接口来规定类的比较规则,或者定义 Serializable 接口来标识类的可序列化特性。
2. 实现多继承: 当一个类需要具备多个类型的行为时,可以通过实现多个接口来达到多继承的效果。
3. 实现回调函数: 当需要实现某种事件监听或回调机制时,可以使用接口作为回调的载体,实现事件的通知和处理。
抽象类的应用场景:
1. 对类的共性特征进行抽象:当某些类具有共同的行为,但又有部分行为不同需要具体实现时,可以使用抽象类来定义这些共性特征,将公共的方法实现在抽象类中。
2. 作为模板类:当需要为某个类的子类提供模板方法,即定义一个算法的框架,将算法的具体实现延迟到子类中时,可以使用抽象类。
3. 代码复用和扩展: 抽象类可以用作其他类的基类,子类必须实现抽象类中的抽象方法,或者将自己定义为抽象类,可以达到代码的复用和扩展效果。
两者应该如何选择:
- 当需要定义一组规范、契约或者多继承的情况下,应该选择接口。
- 当需要定义类的共性特征、提供模板方法或者作为其他类的基类时,应该选择抽象类。
004."==" 和 equals() 的区别是什么?
当在 Java 中使用 "==" 运算符和 equals() 方法时,它们的作用和区别如下:
1. "==" 运算符:
- "==" 运算符用于比较两个对象的引用是否指向内存中的同一个对象实例。
- 当使用 "==" 运算符比较两个对象的引用时,它会检查两个对象是否具有相同的内存地址。
- 对于基本数据类型(如 int、float 等),"==" 运算符比较的是它们的值。
- 对于引用类型(对象),"==" 比较的是它们在内存中的存储地址,即两个引用是否指向同一个对象实例。
示例:
String str1 = new String("hello");
String str2 = str1; // 将str2指向str1的引用
String str3 = new String("hello");
System.out.println(str1 == str2); // true,因为str1和str2引用的是同一个String对象实例
System.out.println(str1 == str3); // false,因为str1和str3引用的是不同的String对象实例
2. equals() 方法:
- equals() 方法是定义在 Object 类中的方法,用于比较对象的内容是否相等。
- 在 Object 类中,equals() 方法的默认实现是使用"=="运算符进行比较,即比较对象的内存地址是否相等。
- 许多类在继承 Object 类的基础上重写了 equals() 方法,使其具有比较对象内容是否相等的功能。
示例:
String str1 = new String("hello");
String str2 = new String("hello");
// true,因为String类重写了equals()方法,比较的是对象的内容是否相等
System.out.println(str1.equals(str2));
总结:
- "==" 用于比较基本数据类型的值和对象的引用是否相同。
- equals() 方法没有被重写的时候的本质就是"==",但一般都会重写(例如String、Integer 等),equals() 方法被重写后,一般用于比较对象的内容是否相等。在实际使用中,需要根据具体的业务需求和对象类型来选择合适的比较方式。一般使用"=="进行引用比较,使用 equals() 进行内容比较。
005.重载(Overloading)和重写(Override)的区别是什么?
对实习 /应届来说,这个几乎必问,
1. 先说重载(Overloading):
- 重载是指在同一个类中定义多个方法,它们具有相同的名称但具有不同的参数列表。
- 重载方法可以有不同的参数类型、不同的参数个数或者不同的参数顺序。
- 在调用重载方法时,编译器根据参数匹配的规则来确定具体调用哪个方法。
- 重载的目的是为了提供不同的方法签名,以便根据传入的不同参数类型或个数来执行相应的操作。
-构成方法重载的底层思维是:在方法调用的时候,Java底层能否区分开这几个同名的方法,如果能,就能构成重载。否则就不能称为重载。
示例:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public float add(float a, float b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
Calculator calculator = new Calculator();
int result1 = calculator.add(1, 2); // 调用第一个add方法
float result2 = calculator.add(1.5f, 2.5f); // 调用第二个add方法
int result3 = calculator.add(1, 2, 3); // 调用第三个add方法
2. 重写(Override):
- 重写是指在子类中覆盖父类的方法,使得子类可以定义自己特有的实现方式。
- 重写要求子类方法的名称、参数列表和返回类型与父类方法完全相同。
- 重写方法必须有相同或更宽松的访问权限,不能有更严格的访问权限。
- 重写方法可以使用 `@Override` 注解来提醒编译器检查是否正确地重写了父类方法。
- 重写方法的目的是在子类中改变父类方法的实现,以满足子类需求,同时保留方法的统一接口。示例:
public class Animal {
public void makeSound() {
System.out.println("Animal makes sound.");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks.");
}
}
Animal animal = new Animal();
animal.makeSound(); // 输出 "Animal makes sound."
Dog dog = new Dog();
dog.makeSound(); // 输出 "Dog barks."
重载(Overloading)和重写(Override),
它们的区别可以总结如下:
1. 定义位置:
- 重载方法是在同一个类中定义多个具有相同名称但参数列表不同的方法。
- 重写方法是在子类中覆盖(重新定义)继承自父类的方法。
2. 参数列表:
- 重载方法具有不同的参数列表,包括参数类型、参数个数或者参数顺序可以不同。
- 重写方法具有相同的参数列表(参数类型和顺序必须一致),但可以有不同的实现。
3. 返回类型:
- 重载方法的返回类型可以相同也可以不同。
- 重写方法的返回类型必须与被重写的方法相同,或者是其子类型。
4. 目的:
- 重载的目的是为了提供更多的方法签名,以便根据传入的参数类型或个数来执行对应的操作。
- 重写的目的是在子类中改变父类方法的实现,以满足子类的特定需求。
5. 多态性:
- 重载是编译时多态(静态多态),根据调用时传入的参数类型来确定具体调用哪个方法。
- 重写是运行时多态(动态多态),根据对象的实际类型来决定调用的方法。
6. 访问权限:
- 重载方法可以有不同的访问权限,可以是公共的、受保护的或私有的。
- 重写方法必须具有相同或更宽松的访问权限(即不能比父类方法更严格)。
重载和重写分别适用于类内部的多个方法以及父子类之间的方法关系。重载通过方法的签名(参数列表)来区分不同的方法;重写则是子类对父类方法的重新定义,以实现特定的行为需求。这两种技术都提供了更灵活的编程手段,并且都与多态的概念密切相关。
006.Java的创建对象的方式有几种?
在Java中,以下是一些常见的创建对象的方式:
1. 使用new关键字:
- 使用`new`关键字是最基本和常见的对象创建方式。
- 通过调用类的构造函数,可以实例化一个新的对象。
- 构造函数可以是默认构造函数(无参数)或带有参数的构造函数。
- 示例中的`MyClass`是类名,`obj`是对象引用变量名,`new MyClass()`创建并分配内存空间。
- 示例:
MyClass obj = new MyClass();
2. 通过反射创建对象:
- Java的反射机制允许在运行时动态地创建对象。
- 可以使用`Class`类的`newInstance`方法或者`Constructor`类的`newInstance`方法来创建对象。
- 需要注意的是,使用反射创建对象时,必须处理`InstantiationException`和`IllegalAccessException`等异常。
- 示例中的`MyClass.class`获取了`MyClass`类的`Class`对象,然后使用`newInstance`方法创建新的对象。
Class clazz = MyClass.class;
MyClass obj = (MyClass) clazz.newInstance();
3. 使用对象克隆:
- 对象克隆是通过对象的`clone`方法实现的。
- 在使用对象克隆时,需要确保类实现了`Cloneable`接口,并重写`clone`方法。
- `clone`方法创建一个与原始对象相似的新对象,但是内存地址不同。
- 示例中的`obj.clone()`使用了对象的`clone`方法创建一个新的对象。
MyClass obj = new MyClass();
MyClass newObj = obj.clone();
4. 使用工厂方法:
- 工厂方法模式通过调用工厂方法来创建对象,而不是直接调用构造函数。
- 工厂方法可以根据一些条件或参数来决定如何创建对象。
- 工厂方法可以隐藏对象创建的细节,并提供更好的对象构建灵活性。
- 示例中的`MyClassFactory.createObject()`是一个静态工厂方法,用于创建`MyClass`对象。
public class MyClassFactory {
public static MyClass createObject() {
return new MyClass();
}
}
MyClass obj = MyClassFactory.createObject();
5. 使用Builder模式:
- Builder模式通常用于创建复杂对象。
- 通过使用Builder模式,可以隐藏对象构建的细节,并提供更好的对象构建灵活性。
- 一般流程是先创建一个Builder类,然后通过链式调用Builder类的方法来设置对象的属性,最后调用`build`方法来创建目标对象。
- 示例中的`MyClass.Builder().setField1(value1).setField2(value2).build()`使用了Builder模式创建了一个`MyClass`对象。
MyClass obj = new MyClass.Builder().setField1(value1).setField2(value2).build();
6. 通过序列化和反序列化:
- 序列化是将对象转换为字节流数据的过程,反序列化则是将字节流数据还原为对象的过程。
- 序列化和反序列化可以通过ObjectOutputStream和ObjectInputStream类来实现。
- 可以将序列化后的字节流保存到文件或通过网络传输。
- 示例中的`ObjectOutputStream`用于将对象写入文件,而`ObjectInputStream`用于从文件中读取并反序列化对象。
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
out.writeObject(myObject);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
MyClass obj = (MyClass) in.readObject();
in.close();
007.说一下你理解的反射吧?
反射是指在运行时动态地检查类、对象、字段和方法的能力。它允许程序在运行时获取到一个类的信息,并且可以实例化对象、调用方法,以及访问和修改字段等操作。
获取类信息
通过反射可以获取到关于类的各种信息,包括类的名称、修饰符、继承关系、实现的接口、构造函数、字段和方法等。可以使用`Class`类提供的方法来获取类的信息,比如:
Class<?> clazz = MyClass.class; // 获取Class对象
String className = clazz.getName(); // 获取类的名称
int modifiers = clazz.getModifiers(); // 获取修饰符
Class<?> superClass = clazz.getSuperclass(); // 获取父类
Class<?>[] interfaces = clazz.getInterfaces(); // 获取实现的接口
Constructor<?>[] constructors = clazz.getConstructors(); // 获取构造函数
Field[] fields = clazz.getDeclaredFields(); // 获取字段
Method[] methods = clazz.getDeclaredMethods(); // 获取方法
创建对象
通过反射可以在运行时动态地实例化对象,不需要在编译期间就知道对象的确切类型。可以使用`newInstance()`方法来实例化对象,或者使用`Constructor`类的`newInstance()`方法来创建对象。例如:
Class<?> clazz = MyClass.class;
Object obj1 = clazz.newInstance(); // 使用newInstance()创建对象
Constructor<?> constructor = clazz.getConstructor(String.class);
Object obj2 = constructor.newInstance("example"); // 使用Constructor的newInstance()创建对象
调用方法
通过反射可以在运行时动态地调用类的方法。可以使用`getMethod()`、`getDeclaredMethod()`等方法来获取类的方法,然后通过`Method`类的`invoke()`方法来调用方法。例如:
Class<?> clazz = MyClass.class;
Object obj = clazz.newInstance();
Method method = clazz.getMethod("methodName", parameterTypes);
method.invoke(obj, args);
访问和修改字段
使用反射可以在运行时访问和修改类的字段(成员变量)。可以使用`getField()`、`getDeclaredField()`等方法来获取字段,然后通过`Field`类的`get()`、`set()`等方法来进行操作。例如:
Class<?> clazz = MyClass.class;
Object obj = clazz.newInstance();
Field field = clazz.getField("fieldName");
field.set(obj, value); // 设置字段的值
Object value = field.get(obj); // 获取字段的值
动态代理
反射机制还为动态代理提供了可能,可以通过`Proxy`类和`InvocationHandler`接口创建动态代理对象,实现对对象的代理访问。
反射常见的应用场景:
1. 框架和库的开发:反射在框架和库的设计中起到了至关重要的作用。通过反射,框架可以在运行时扫描和加载类,实现插件化的功能。它可以动态创建对象、调用方法、访问字段和构造器,从而实现动态扩展和配置化的功能。
2. 配置文件处理:反射可以帮助读取和解析配置文件。通过反射,可以根据配置文件中的类名自动加载对象,并设置属性和调用方法。这样可以实现灵活的配置和可扩展的功能。
3. 注解处理器:反射在注解处理器中广泛应用。注解处理器可以通过反射来解析和处理注解信息。例如,通过反射可以获取类、方法、字段等的注解,并根据注解信息生成特定的代码。
4. 单元测试:反射在单元测试中起到了重要的作用。通过反射,测试框架可以动态地获取类的方法和字段信息,并调用它们进行测试。这样可以提高测试的灵活性和扩展性。
5. 动态代理:反射可以实现动态代理的功能。通过反射,可以在运行时生成代理类,并将方法调用转发给实际的对象。这样可以实现AOP(面向切面编程)等功能。
反射的优点和一些缺点。
好处:
1. 动态性:反射允许程序在运行时动态地获取类的信息并操作对象,这样可以实现灵活的配置和动态扩展,使得程序更加灵活和可配置。
2. 框架和库的设计:反射使得框架和库更具有通用性和灵活性,可以实现插件化的功能,允许用户扩展和定制功能。
3. 减少重复代码:反射可以帮助减少重复代码的编写,在某些情况下可以减少开发工作量。
4. 实现通用性代码:通过反射可以实现通用性的代码,例如实现JSON序列化和反序列化,实现动态代理等。
缺点:
1. 性能开销:使用反射会带来较大的性能开销,因为在运行时需要动态解析并调用方法、访问字段等,远不如直接调用方法效率高。
2. 安全性问题:由于反射可以访问和修改类的私有成员,可能存在安全性隐患,需要进行适当的权限检查和控制。
3. 难以调试:由于反射操作是在运行时动态进行的,因此在程序调试和定位问题时可能会增加一定的复杂性。
4. 破坏封装性:反射可以访问和修改类的私有成员,可能破坏类的封装性,增加代码的维护难度和风险。
都看到这了,说明你也是一个有毅力的人,那还不关注一下??
让我继续带你看下一期的【精品】??