1 面向对象编程有哪些特性?
面向对象编程(Object-Oriented Programming,简称 OOP)是一种以对象为核心的编程范式,它通过模拟现实世界中的事物及其关系来组织代码。OOP 具有三大核心特性:封装、继承、多态。接下来我会逐一详细说明这些特性。
第一,封装
封装是指将数据(属性)和行为(方法)捆绑在一起,并对外隐藏对象的内部实现细节。通过访问修饰符(如 private、protected 和 public),我们可以控制哪些部分是对外可见的,哪些是内部私有的。这种机制提高了代码的安全性和可维护性。例如,在 Java 中,我们通常会将类的属性设置为 private,并通过 getter 和 setter 方法提供受控的访问方式。
第二,继承
继承允许一个类(子类)基于另一个类(父类)来构建,从而复用父类的属性和方法。通过继承,子类不仅可以拥有父类的功能,还可以扩展或重写父类的行为。Java 中使用 extends 关键字实现继承。例如,我们可以通过定义一个通用的 Animal 类,然后让 Dog 和 Cat 类继承它,这样就避免重复编写相同的代码。
第三,多态
多态是指同一个方法调用可以根据对象的实际类型表现出不同的行为。多态分为两种形式:编译时多态(方法重载)和运行时多态(方法重写)。运行时多态是通过动态绑定实现的,即程序在运行时决定调用哪个方法。例如,如果父类 Animal 有一个 eat() 方法,子类 Dog 和 Cat 可以分别重写这个方法,当调用 animal.eat() 时,具体执行的是 Dog 或 Cat 的实现。多态使得代码更加灵活和可扩展。
延伸
面向对象和面向过程的区别
(1)面向对象编程(OOP)
定义:面向对象编程是一种以对象为核心的编程范式,它将现实世界中的事物抽象为对象,并通过对象之间的交互来完成任务。OOP 的核心思想是“万物皆对象”。
特点:
以对象为中心,强调数据和行为的封装。
使用类(Class)作为模板来创建对象(Object)。
支持三大特性:封装、继承和多态。
优点:
模块化:代码结构清晰,易于维护和扩展。
复用性高:通过继承和组合可以复用已有代码。
灵活性强:支持多态,能够适应复杂需求的变化。
可读性强:贴近现实世界的思维方式,便于理解和协作。
缺点:
性能开销较大:由于引入了类、对象等概念,运行效率可能不如面向过程高效。
学习曲线较陡:需要理解类、对象、继承等概念,初学者可能觉得复杂。
(2)面向过程编程(POP)
定义:
面向过程编程是一种以过程(函数)为核心的编程范式,它将程序分解为一系列函数或步骤,按照顺序执行任务。
特点:
以函数为中心,强调逻辑流程。
数据和函数分离,通常通过参数传递数据。
程序由一个个独立的函数组成,函数之间通过调用关系协作。
优点:
简单直接:逻辑清晰,适合小型项目或简单任务。
性能较高:没有额外的对象开销,运行效率更高。
易于实现:不需要复杂的类设计,开发速度快。
缺点:
扩展性差:随着项目规模增大,代码容易变得混乱,难以维护。
复用性低:函数之间的耦合度高,难以复用代码。
不适合复杂系统:面对复杂业务逻辑时,代码会显得冗长且难以管理。
(3)举例
- 面向过程:实现一个简单计算器,用函数依次处理加减乘除。
-
public class Calculator {public static int add(int a, int b) {return a + b;}public static int subtract(int a, int b) {return a - b;}public static void main(String[] args) {System.out.println(add(3, 5)); // 输出 8} }
- 面向对象:为计算器定义一个类,通过实例化对象调用方法。
-
public class Calculator {private int result;public void add(int a) {result += a;}public int getResult() {return result;}public static void main(String[] args) {Calculator calc = new Calculator();calc.add(5);System.out.println(calc.getResult()); // 输出 5} }
2 接口、普通类和抽象类的区别和共同点
在 Java 中,接口、普通类和抽象类是构建面向对象程序的三种重要结构。我会从定义、方法实现、继承关系以及成员变量这4个方面详细讲解它们的区别,然后再总结它们的共同点。
第一个是定义上的区别
普通类是一个完整的、具体的类,可以直接实例化为对象。它包含属性和方法,并且可以有构造方法。
抽象类是一个不能直接实例化的类,通常用来作为其他类的基类。它可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。
接口是一种完全抽象的结构,用于定义行为规范。它只包含抽象方法(Java 8 之后可以包含默认方法和静态方法)。
第二个是方法实现上的区别
普通类的所有方法都可以有具体实现(即方法体)。
抽象类可以包含具体方法和抽象方法。
接口默认只包含抽象方法(Java 8 后可以包含默认方法和静态方法)。
第三是继承关系上的区别
普通类支持单继承(一个类只能继承一个父类)。
抽象类也支持单继承(一个类只能继承一个抽象类)。
接口支持多实现(一个类可以实现多个接口)。
第四是成员变量上的区别
普通类和抽象类都可以有各种类型的成员变量(实例变量、静态变量等)。
接口只能有常量(public static final)。
最后是共同点,一共有3点
首先,它们都是面向对象编程的基础结构,都可以用来组织代码,实现封装、继承和多态等特性。
其次,它们都可以包含方法,尽管接口中的方法默认是抽象的。
最后,它们都可以被继承或实现,普通类可以通过继承扩展功能,抽象类和接口则需要子类继承或实现后才能使用。
3 深拷贝和浅拷贝区别
深拷贝和浅拷贝的核心区别在于是否递归地复制对象内部的引用类型数据,接下来,我会从定义、实现方式以及使用场景三个方面详细讲解它们的区别。
首先是定义上的区别,
浅拷贝是指创建一个新对象,但新对象中的引用类型字段仍然指向原对象中引用类型的内存地址。换句话说,浅拷贝只复制了对象本身,而没有复制对象内部的引用类型数据。修改新对象中的引用类型数据会影响原对象。
深拷贝是指创建一个新对象,并且递归地复制对象内部的所有引用类型数据。换句话说,深拷贝不仅复制了对象本身,还复制了对象内部的所有引用类型数据。修改新对象中的引用类型数据不会影响原对象。
其次是实现方式上的区别,
浅拷贝可以使用 Object 类的 clone() 方法,也可以使用实现 Cloneable 接口并重写 clone() 的方法。
深拷贝可以手动对引用类型字段进行递归拷贝,也可以使用序列化(Serialization)的方式将对象序列化为字节流,再反序列化为新对象。
最后是使用场景上的区别,
浅拷贝适用于当对象内部的引用类型数据不需要独立复制的情况。
深拷贝适用于当对象内部的引用类型数据需要完全独立的情况。
延伸
1,浅拷贝使用方法
public class Resume implements Cloneable {public Object clone() {try {return (Resume) super.clone();} catch (Exception e) {e.printStackTrace();return null;}}
}
2,深拷贝使用方法
class Student implements Cloneable {String name;int age;Professor p;Student(String name, int age, Professor p) {this.name = name;this.age = age;this.p = p;}public Object clone() {Student o = null;try {o = (Student) super.clone();} catch (CloneNotSupportedException e) {System.out.println(e.toString());}o.p = (Professor) p.clone();return o;}
}
4 int和Integer的区别
Integer 和 int 的区别如下:
类型
int 是基本数据类型,存储的是数值。
Integer 是 int 的包装类,是一个对象,存储的是 int 值的引用。
内存分配
int 存储在栈中,效率高,直接存储数值。
Integer 是对象,存储在堆中,效率相对较低。
用途
int 用于基本的数值运算。
Integer 提供了更多功能,比如可以与集合类(List、Map 等)一起使用,因为集合类只能存储对象。
延伸
1 自动装箱和拆箱
从 Java 5 开始,Java 提供了自动装箱和拆箱功能,Java 自动支持基本类型与其包装类之间的转换(自动装箱和拆箱)。
装箱:int 转换为 Integer。
拆箱:Integer 转换为 int。
自动装箱是指将基本数据类型(如 int、double、boolean 等)自动转换为对应的包装类对象(如 Integer、Double、Boolean 等)。这个过程由编译器自动完成,无需手动调用包装类的构造方法或静态方法。
当存储一个基本数据类型到需要用到对象的场景中(例如集合),Java 编译器会检测到基本数据类型需要被转换为包装类对象,编译器会自动调用包装类的 valueOf() 方法来创建对应的包装类对象,生成的对象会被存储到目标位置。
自动拆箱是指将包装类对象(如 Integer、Double、Boolean 等)自动转换为对应的基本数据类型(如 int、double、boolean 等)。同样,这个过程也是由编译器自动完成的。
当你从一个需要对象的场景中取出值并赋给基本数据类型时,Java 编译器会检测到目标变量是一个基本数据类型。编译器会自动调用包装类的 xxxValue() 方法,比如 intValue()、doubleValue() 等,来获取基本数据类型的值。返回的基本数据类型值会被赋给目标变量。
2 内存分配
int:
存储在栈内存中,直接保存值,效率高。
Integer:
是一个对象,存储在堆内存中,包含元数据(如值和方法),占用更多内存。
3 Integer 缓存机制
Java 对于 -128 到 127 之间的 Integer 对象进行了缓存。
当值在该范围内时,会直接返回缓存中的对象。
如果值超出范围,会创建新的对象。
public class Main {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a b); // true,使用缓存
System.out.println(c d); // false,超出缓存范围
}
}
输出结果:true
false
4 与集合的结合使用
int 不能直接存储在集合中,必须使用 Integer。
5 重载和重写的区别
重载(Overloading)和重写(Overriding)是 Java 中两个非常重要的概念,用于实现多态。它们的区别如下:
- 定义:
- 重载:同一个类中方法名称相同,但参数列表(参数个数或类型)不同。
- 重写:子类对父类方法进行重新定义,方法名、参数列表都相同。
- 作用域:
- 重载:只发生在同一个类中。
- 重写:发生在子类和父类之间。
- 方法签名:
- 重载:方法名相同,参数列表不同。
- 重写:方法名、参数列表相同。
- 返回值:
- 重载:返回值可以相同或不同。
- 重写:返回值必须与父类方法兼容(从 Java 5 开始可以是协变返回类型)。
- 访问修饰符:
- 重载:不受访问修饰符限制。
- 重写:子类方法的访问修饰符不能比父类方法的访问修饰符更严格。
- 关键字:
- 重写:需要用到
@Override
注解。 - 重载:无需特殊注解。
- 重写:需要用到
延伸
1.什么是重载?
重载是指在同一个类中,允许存在多个同名方法,但这些方法的参数列表必须不同。重载的核心在于方法签名的不同,而返回值类型不影响重载。
当定义一个重载方法时,
首先,方法名必须相同。
其次,参数列表必须不同,包括参数的数量、类型或顺序。
然后,返回值类型可以相同也可以不同,但它不影响重载的判断。
最后,访问修饰符(如 public
、private
等)和异常声明也不影响重载。
2.什么是重写?
重写是指子类对父类中已有的方法进行重新定义,以提供特定的实现。重写的核心在于**继承关系**,并且要求方法签名完全一致。
当定义一个重写方法时,
首先,方法名必须与父类中的方法名相同。
其次,参数列表必须与父类中的方法完全一致。
然后,返回值类型必须相同或是父类返回值类型的子类型(协变返回类型)。
最后,访问修饰符不能比父类更严格(例如,父类方法是 protected,子类方法可以是 protected 或 public,但不能是 private)。
3.重载的细节
重载是方法的编译时多态,即方法的调用在编译期由参数列表决定。以下是一个示例:
public class OverloadingExample {public int add(int a, int b) {return a + b;}public double add(double a, double b) {return a + b;}public String add(String a, String b) {return a + b;}
}这里 add 方法重载了三次,分别接收不同类型的参数。
在编译时,编译器会根据传入的参数类型选择对应的方法。
4.重写的细节
重写是方法的运行时多态,由子类在运行期决定具体调用哪个方法。以下是一个示例:
class Parent {public void display() {System.out.println("Parent display method");}
}class Child extends Parent {@Overridepublic void display() {System.out.println("Child display method");}
}
public class OverridingExample {public static void main(String[] args) {Parent obj = new Child();obj.display(); // 输出:Child display method}
}
这里 Child 类重写了 Parent 类的 display 方法。
在运行时,即使引用类型是 Parent,实际调用的是子类的 display 方法。
6 ==和 equals 的区别
==
比较的是 两个变量的内存地址,即是否引用了同一个对象。如果是基本数据类型,==
比较的是它们的值。equals()
是 对象的比较方法,默认实现(在Object
类中)是比较内存地址,和==
类似。但是,大多数类(例如String
、Integer
等)会重写equals()
方法,用于比较内容(值)是否相同。
示例代码:
public class Main {public static void main(String[] args) {String str1 = new String("hello");String str2 = new String("hello");String str3 = "hello";String str4 = "hello";// == 比较System.out.println(str1 == str2); // false,两个对象的内存地址不同System.out.println(str3 == str4); // true,字符串常量池优化// equals 比较System.out.println(str1.equals(str2)); // true,内容相同System.out.println(str3.equals(str4)); // true,内容相同}
}
延伸
1.为什么需要重写 equals()
方法?
- 默认的equals()方法比较的是对象地址,而在许多场景下,我们需要比较对象的内容是否相等。例如:比较两个
Person
对象的name
和age
是否相等。 - 当你重写
equals()
方法时,一般还需要重写hashCode()
方法,保证逻辑一致。
2.hashCode()
和 equals()
的关系
- 如果两个对象通过
equals()
方法相等,它们的hashCode()
必须相等。 - 如果两个对象的
hashCode()
相等,它们未必通过equals()
方法相等。 - 这条规则对于集合(如
HashMap
和HashSet
)非常重要。
示例:hashCode()
与 equals()
的一致性
class Person {private String name;public Person(String name) {this.name = name;}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;Person person = (Person) obj;return name.equals(person.name);}@Overridepublic int hashCode() {return name.hashCode();}
}
3.hashCode() 的作用
hashCode() 是 Java 中 Object 类的一个方法,用于返回对象的哈希码(Hash Code),它是一个整数值。这个哈希码的主要作用是确定对象在基于哈希表的数据结构中的存储位置。
(1) 哈希码的核心作用:定位对象
快速定位:
在基于哈希表的数据结构(如 HashMap、HashSet 和 Hashtable)中,hashCode() 被用来计算对象的存储位置。
哈希表通过将对象的哈希值映射到特定的“桶”(Bucket)中,从而实现高效的插入、查找和删除操作。
这种机制使得哈希表的时间复杂度通常为 O(1),即常数时间复杂度,极大提升了性能。
7 什么是泛型?有什么作用?
泛型(Generics) 是 Java 中的一种语言特性,允许我们在类、接口和方法中使用参数化类型。它提供了一种在编译时检测类型安全的机制,从而减少运行时的类型转换错误。泛型的主要目的是使代码更加类型安全、可读性强、通用性高。
泛型的作用主要有4点
第一点是提高代码的复用性,它允许我们编写与类型无关的通用代码。
第二点是增强类型安全性,在没有泛型的情况下,集合类(如 ArrayList)默认存储的是 Object 类型,取出元素时需要手动进行类型转换,容易引发 ClassCastException。而泛型在编译时就会进行类型检查,避免了运行时的类型错误。
第三点是简化代码,使用泛型后,我们无需显式地进行类型转换,减少了冗余代码,提高了代码的可读性和维护性。
第四点是支持复杂的类型约束,泛型可以通过通配符(如 ? extends T 和 ? super T)实现更复杂的类型限制,满足特定场景下的需求。
延伸
1.泛型的语法与使用
泛型类 泛型可以用于定义类,使类可以处理多种数据类型。
例子:
public class Box<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}
}public class Example {public static void main(String[] args) {Box<String> stringBox = new Box<>();stringBox.setValue("Hello");System.out.println(stringBox.getValue()); // 输出:HelloBox<Integer> intBox = new Box<>();intBox.setValue(123);System.out.println(intBox.getValue()); // 输出:123}
}
T 是一个类型参数,表示该类可以接受任意类型。
使用时,通过指定具体类型(如String、Integer)实现类型安全。
泛型方法 泛型也可以用于方法,使方法可以接受不同类型的参数。
例子:
public class Example {// 泛型方法public static <T> void printArray(T[] array) {for (T element : array) {System.out.println(element);}}public static void main(String[] args) {Integer[] intArray = {1, 2, 3};String[] strArray = {"A", "B", "C"};printArray(intArray); // 输出:1 2 3printArray(strArray); // 输出:A B C}
}方法前面的<T>表示这是一个泛型方法,T是类型参数。
方法可以接受任意类型的数组作为参数。
泛型接口 泛型可以用于接口,使接口适配不同的实现类型。
例子:
public interface Pair<K, V> {K getKey();V getValue();
}public class KeyValue<K, V> implements Pair<K, V> {private K key;private V value;public KeyValue(K key, V value) {this.key = key;this.value = value;}public K getKey() {return key;}public V getValue() {return value;}
}public class Example {public static void main(String[] args) {Pair<String, Integer> pair = new KeyValue<>("Age", 25);System.out.println("Key: " + pair.getKey()); // 输出:Key: AgeSystem.out.println("Value: " + pair.getValue()); // 输出:Value: 25}
}
泛型接口允许在实现时指定不同的类型参数。
通配符:泛型可以使用通配符(?
)来表示未知类型
-
通配符的使用:
? extends T
:表示类型是T
或T
的子类(用于读取)。? super T
:表示类型是T
或T
的父类(用于写入)。-
?
:表示任意类型。例子:
import java.util.ArrayList; import java.util.List;public class Example {public static void printNumbers(List<? extends Number> list) {for (Number number : list) {System.out.println(number);}}public static void main(String[] args) {List<Integer> intList = new ArrayList<>();intList.add(1);intList.add(2);List<Double> doubleList = new ArrayList<>();doubleList.add(1.1);doubleList.add(2.2);printNumbers(intList); // 输出:1 2printNumbers(doubleList); // 输出:1.1 2.2} }? extends Number 表示列表中的元素必须是Number或其子类,保证类型安全。
2.泛型的限制
- Java 的泛型是通过类型擦除(Type Erasure)实现的,泛型信息只存在于编译阶段,运行时被擦除为
Object
。 - 例如,
List<Integer>
和List<String>
在运行时实际上是相同的类型。例子:
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // 输出:true
3.泛型不能使用基本数据类型
- 泛型不支持基本数据类型(如
int
、double
等),只能使用包装类(如Integer
、Double
等)。 - 解决方案:自动装箱和拆箱。
4.泛型数组的创建
不能直接创建泛型数组,如T[]
,因为类型信息在运行时被擦除。
8 什么是反射?应用?
反射是一种在运行时动态获取类信息的能力。通过反射,我们可以在程序运行时加载类、获取类的结构(如字段、方法、构造器等),甚至可以调用类的方法或修改字段的值。
反射主要应用在这5个场景
第一个是框架开发,很多 Java 框架都有使用反射,比如如 Spring、Hibernate 等。
第二个是动态代理,动态代理是反射的一个重要应用,常用于 AOP(面向切面编程)。通过反射,我们可以在运行时动态生成代理类,拦截方法调用并添加额外逻辑。
第三个是注解处理,注解本身不会对程序产生任何影响,但通过反射,我们可以在运行时读取注解信息并执行相应的逻辑。
第四个是插件化开发,在某些场景下,我们需要动态加载外部的类或模块。反射可以帮助我们在运行时加载这些类并调用其方法,从而实现插件化开发。
第五个是测试工具,单元测试框架(如 JUnit)利用反射来发现和运行测试方法,而无需手动指定每个测试用例。
延伸
1.为什么引入反射
在传统的 Java 编程中,所有的类和方法调用必须在编译时确定。 这种模式虽然安全且高效,但在某些场景下显得不够灵活。而Java 引入反射是为了在运行时动态获取类的信息并操作类或对象,包括获取类的结构、创建对象、调用方法、访问 / 修改字段等,从而增强程序的灵活性和可扩展性。
2.反射的优缺点
优点:
灵活性强:允许程序动态操作未知类,非常适合框架开发或插件化设计。
动态性:反射支持运行时动态加载类和调用方法,避免了硬编码。
代码复用性高:通过反射,可以写出通用代码,减少重复。
缺点:
性能开销大:反射的调用比直接调用慢得多,因为需要进行许多运行时检查和处理。
安全风险:反射能绕过 Java 的访问控制机制,如访问私有字段或方法,可能导致安全问题。
代码可读性差:反射的代码往往复杂且难以维护,尤其对于大型项目而言。
编译时安全性降低:使用反射时,编译器无法验证被调用的方法或字段是否存在。
9 StringBuffer 的特点
StringBuffer 是一个可变的字符序列,与 String 不同,StringBuffer 的内容是可以被修改的。它的核心特点是线程安全和高效的字符串操作。
StringBuffer有四个特点:
第一个是它具有可变性,我们可以在原有对象上直接修改字符串内容,而无需创建新的对象。
第二个它是线程安全的,StringBuffer 的所有方法都通过 synchronized 关键字修饰,因此它是线程安全的。 在多线程环境下,多个线程可以同时操作同一个 StringBuffer 对象,而不会引发数据竞争或不一致问题。
第三个是性能相对较好,StringBuffer 内部使用一个可扩容的字符数组来存储数据,当容量不足时会自动扩展。相比于 String 的不可变性(每次修改都会生成新对象),StringBuffer 在频繁修改字符串时性能更高。而相比于非线程安全的 StringBuilder ,性能略低。
第四个是包含丰富的 API,比如:append():追加内容到字符串末尾。 insert():在指定位置插入内容。delete():删除指定范围的内容。 reverse():反转字符串内容。 toString():将 StringBuffer 转换为 String。
延伸
1.String、StringBuffer、StringBuilder 的区别?
String
- 不可变:
String
是不可变类,每次对字符串的修改都会生成新的字符串对象。 - 线程安全:因为不可变,所以天然线程安全。
- 性能:由于每次修改都会创建新对象,频繁操作时效率较低。
- 不可变:
StringBuilder
- 可变:
StringBuilder
是可变类,对字符串的操作会直接在原对象上修改。 - 线程不安全:不适用于多线程场景。
- 性能:在单线程环境中,操作效率比
String
和StringBuffer
高。
- 可变:
StringBuffer
- 可变:和
StringBuilder
类似,也是可变类。 - 线程安全:通过同步方法实现线程安全,适用于多线程场景。
- 性能:由于线程安全机制的开销,操作效率比
StringBuilder
略低。
- 可变:和
适用场景:
- 如果字符串内容 不会改变,用
String
。 - 如果字符串内容会频繁修改,且在 单线程 环境下使用,选
StringBuilder
。 - 如果字符串内容会频繁修改,且在 多线程 环境下使用,选
StringBuffer
。