怎么理解面向对象?简单说说封装继承多态
面向对象是一种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。
Java面向对象的三大特性包括:封装、继承、多态:
- 封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。
- 继承:继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。
- 多态:多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。
多态体现在哪几个方面?
多态在面向对象编程中可以体现在以下几个方面:
- 方法重载:
- 方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。
- 方法重写:
- 方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。
- 接口与实现:
- 多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。
- 向上转型和向下转型:
- 在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。
- 向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 ClassCastException。
多态解决了什么问题?
多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。
多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。
面向对象的设计原则你知道有哪些吗
面向对象编程中的六大原则:
- 单一职责原则:一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。
- 开放封闭原则:软件实体应该对扩展开放,对修改封闭。
- 里氏替换原则:子类对象应该能够替换掉所有父类对象。
- 接口隔离原则:客户端不应该依赖那些它不需要的接口,即接口应该小而专。
- 依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
- 最少知识原则:一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。
重载与重写有什么区别?
- 重载指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表,编译器根据调用时的参数类型来决定调用哪个方法。
- 重写指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@Override注解来明确表示这是对父类方法的重写。
重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。
抽象类和普通类区别?
- 实例化:普通类可以直接实例化对象,而抽象类不能被实例化,只能被继承。
- 方法实现:普通类中的方法可以有具体的实现,而抽象类中的方法可以有实现也可以没有实现。
- 继承:一个类可以继承一个普通类,而且可以继承多个接口;而一个类只能继承一个抽象类,但可以同时实现多个接口。
- 实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,被其他类继承和扩展使用。
Java抽象类和接口的区别是什么?
两者的特点:
- 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
- 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8以后可以有默认方法和静态方法)。适用于定义类的能力或功能。
两者的区别:
- 实现方式:实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
- 方法方式:接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
- 访问修饰符:接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
- 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。
抽象类可以被实例化吗?
在Java中,抽象类本身不能被实例化。
这意味着不能使用new关键字直接创建一个抽象类的对象。抽象类的存在主要是为了被继承,它通常包含一个或多个抽象方法,这些方法需要在子类中被实现。
抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。
抽象类能加final修饰吗?
不能,Java中的抽象类是用来被继承的,而final修饰符用于禁止类被继承或方法被重写,因此,抽象类和final修饰符是互斥的,不能同时使用。
接口里面可以定义哪些方法?
- 抽象方法:抽象方法是接口的核心部分,所有实现接口的类都必须实现这些方法。抽象方法默认是public和abstract,这些修饰符可以省略。
- 默认方法:默认方法是在Java 8中引入的,允许接口提供具体实现。实现类可以选择重写默认方法。
- 静态方法:静态方法也是在 Java 8 中引入的,它们属于接口本身,可以通过接口名直接调用,而不需要实现类的对象。
- 私有方法:私有方法是在 Java 9 中引入的,用于在接口中为默认方法或其他私有方法提供辅助功能。这些方法不能被实现类访问,只能在接口内部使用。
接口可以包含构造函数吗?
在接口中,不可以有构造方法,在接口里写入构造方法时,编译器提示:Interfaces cannot have constructors,因为接口不会有自己的实例的,所以不需要有构造函数。
解释Java中的静态变量和静态方法
在Java中,静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。
静态变量是在类中使用static关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点:
- 共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
- 初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
- 访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。
静态方法是在类中使用 static 关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点:
- 无实例依赖:静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。
- 访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
- 多态性:静态方法不支持重写,但可以被隐藏。
使用场景:
- 静态变量:常用于需要在所有对象间共享的数据,如计数器、常量等。
- 静态方法:常用于助手方法、获取类级别的信息或者是没有依赖于实例的数据处理。
非静态内部类和静态内部类的区别?
区别包括:
- 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
- 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
- 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
- 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
- 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。
非静态内部类可以直接访问外部方法,编译器是怎么做到的?
非静态内部类可以直接访问外部方法是因为编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。
这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系,从而实现直接访问外部方法的功能。
== 与 equals 有什么区别?
- ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
- equals():比较的是两个字符串的内容,属于内容比较。
hashCode和equals方法有什么关系?
在Java中,对于重写 equals 方法的类,通常也需要重写 hashCode 方法,并且需要遵循以下规定:
- 一致性:如果两个对象使用 equals 方法比较结果为 true,那么它们的 hashCode 值必须相同。也就是说,如果 obj1.equals(obj2) 返回 true,那么 obj1.hashCode() 必须等于 obj2.hashCode()。
- 非一致性:如果两个对象的 hashCode 值相同,它们使用 equals 方法比较的结果不一定为 true。即 obj1.hashCode() == obj2.hashCode() 时,obj1.equals(obj2) 可能为 false,这种情况称为哈希冲突。
hashCode 和 equals 方法是紧密相关的,重写 equals 方法时必须重写 hashCode 方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode 方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。
String、StringBuffer、StringBuilder的区别和联系
- 可变性:String 是不可变的,一旦创建,内容无法修改,每次修改都会生成一个新的对象。StringBuilder 和 StringBuffer 是可变的,可以直接对字符串内容进行修改而不会创建新对象。
- 线程安全性:String 因为不可变,天然线程安全。StringBuilder 不是线程安全的,适用于单线程环境。StringBuffer 是线程安全的,其方法通过 synchronized 关键字实现同步,适用于多线程环境。
- 性能:String 性能最低,尤其是在频繁修改字符串时会生成大量临时对象,增加内存开销和垃圾回收压力。StringBuilder 性能最高,因为它没有线程安全的开销,适合单线程下的字符串操作。StringBuffer 性能略低于 StringBuilder,因为它的线程安全机制引入了同步开销。
- 使用场景:如果字符串内容固定或不常变化,优先使用 String。如果需要频繁修改字符串且在单线程环境下,使用 StringBuilder。如果需要频繁修改字符串且在多线程环境下,使用 StringBuffer。
Java 8新特性
| 特性名称 | 描述 | 示例或说明 |
|---|---|---|
| Lambda 表达式 | 简化匿名内部类,支持函数式编程 | (a, b) -> a + b 代替匿名类实现接口 |
| 函数式接口 | 仅含一个抽象方法的接口,可用 @FunctionalInterface 注解标记 | Runnable、Comparator,或自定义接口@FunctionalInterface interface MyFunc { void run(); } |
| Stream API | 提供链式操作处理集合数据,支持并行处理 | list.stream().filter(x -> x > 0).collect(Collectors.toList()) |
| Optional 类 | 封装可能为 null 的对象,减少空指针异常 | Optional.ofNullable(value).orElse("default") |
| 方法引用 | 简化 Lambda 表达式,直接引用现有方法 | System.out::println 等价于 x -> System.out.println(x) |
| 接口的默认方法与静态方法 | 接口可定义默认实现和静态方法,增强扩展性 | interface A { default void print() { System.out.println("默认方法"); } } |
| 并行数组排序 | 使用多线程加速数组排序 | Arrays.parallelSort(array) |
| 重复注解 | 允许同一位置多次使用相同注解 | @Repeatable 注解配合容器注解使用 |
| 类型注解 | 注解可应用于更多位置(如泛型、异常等) | List<@NonNull String> list |
| CompletableFuture | 增强异步编程能力,支持链式调用和组合操作 | CompletableFuture.supplyAsync(() -> "result").thenAccept(System.out::println) |
Lambda表达式了解吗?
Lambda表达式它是一种简洁的语法,用于创建匿名函数,主要用于简化函数式接口(只有一个抽象方法的接口)的使用。其基本语法有以下两种形式:
(parameters) -> expression:当Lambda体只有一个表达式时使用,表达式的结果会作为返回值。(parameters) -> { statements; }:当Lambda体包含多条语句时,需要使用大括号将语句括起来,若有返回值则需要使用return语句。
- Lambda表达式可以用更简洁的语法实现相同的功能。
- Lambda 表达式使得 Java 支持函数式编程范式,允许将函数作为参数传递,从而可以编写更灵活、可复用的代码。比如定义一个通用的计算函数。
虽然Lambda表达式优点蛮多的,不过也有一些缺点,比如会增加调试困难,因为Lambda表达式是匿名的,在调试时很难定位具体是哪个Lambda表达式出现了问题。尤其是当Lambda表达式嵌套使用或者比较复杂时,调试难度会进一步增加。
Java中stream的API介绍一下
Java 8引入了Stream API,它提供了一种高效且易于使用的数据处理方式,特别适合集合对象的操作,如过滤、映射、排序等。Stream API不仅可以提高代码的可读性和简洁性,还能利用多核处理器的优势进行并行处理。
Java 21 新特性知道哪些?
- Switch 语句的模式匹配:该功能在 Java 21 中也得到了增强。它允许在 switch 的 case 标签中使用模式匹配,使操作更加灵活和类型安全,减少了样板代码和潜在错误。例如,对于不同类型的账户类,可以在 switch 语句中直接根据账户类型的模式来获取相应的余额,如
case SavingsAccount sa -> result = sa.getSavings(); - 数组模式:将模式匹配扩展到数组中,使开发者能够在条件语句中更高效地解构和检查数组内容。例如,
if (arr instanceof int[] {1, 2, 3}),可以直接判断数组 arr 是否匹配指定的模式。 - 字符串模板(预览版):提供了一种更可读、更易维护的方式来构建复杂字符串,支持在字符串字面量中直接嵌入表达式。例如,以前可能需要使用
"hello " + name + ", welcome to the geeksforgeeks!"这样的方式来拼接字符串,在 Java 21 中可以使用hello {name}, welcome to the geeksforgeeks!这种更简洁的写法
新并发特性方面:
- 虚拟线程:这是 Java 21 引入的一种轻量级并发的新选择。它通过共享堆栈的方式,大大降低了内存消耗,同时提高了应用程序的吞吐量和响应速度。可以使用静态构建方法、构建器或 ExecutorService 来创建和使用虚拟线程。
- Scoped Values(范围值):提供了一种在线程间共享不可变数据的新方式,避免使用传统的线程局部存储,促进了更好的封装性和线程安全,可用于在不通过方法参数传递的情况下,传递上下文信息,如用户会话或配置设置。
Stream流的并行API是什么?
是 ParallelStream。
并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果
Stream串行流与并行流的主要区别:
- 串行流:数据源通过
stream()等方法生成单个流,按顺序执行操作后输出结果; - 并行流:数据源通过
parallelStream()/parallel()生成多个子流,并行执行操作后汇总结果)
对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。
completableFuture怎么用的?
CompletableFuture是由Java 8引入的,在Java8之前我们一般通过Future实现异步。
- Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致回调地狱
- CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。