【Java】基础(二) | OOP & 内部类 & 接口 & 反射 & 注解 & 泛型
Java 的封装、继承、多态是什么?
在Java中,封装、继承和多态是面向对象编程(OOP)的三大基本特性。
封装(Encapsulation)
封装指的是将对象的属性(字段)和行为(方法)封装在一个类中,并通过访问控制(如private
、protected
和public
)来隐藏对象的内部实现细节。这样,外部的代码不能直接访问对象的内部数据,只能通过提供的公共方法(通常是getter和setter方法)来操作数据。
理解:
- 隐藏实现细节:用户不需要知道内部是如何实现的,只需要知道如何使用它。
- 控制访问:通过设置不同的访问修饰符,可以控制哪些数据和方法可以被外部访问。
- 提高安全性:避免了外部直接访问和修改对象的状态,从而防止数据被非法访问或修改。
继承(Inheritance)
继承是一种机制,允许一个类(子类)从另一个类(父类)继承属性和方法。通过继承,子类可以复用父类的代码,并可以在子类中扩展或重写父类的方法。
理解:
- 代码复用:子类可以继承父类的属性和方法,避免重复代码。
- 实现层次结构:通过继承,类之间可以形成一种“是一个”的关系,从而构建类的层次结构。
- 方法重写:子类可以根据需要重写父类的方法,以提供特定的实现。
多态(Polymorphism)
多态指的是同一个方法或对象在不同场景下可以表现出不同的行为。在Java中,多态主要通过方法重载(Overloading)和方法重写(Overriding)实现。
理解:
- 方法重载:同一个类中可以有多个同名方法,但这些方法的参数列表不同。
- 方法重写:子类可以重写父类的方法,以提供特定的实现。
- 接口多态:对象可以通过父类或接口的引用指向子类的实例,调用方法时会根据实际对象类型执行对应的方法。
Java 方法重载和方法重写之间的区别是什么?
- 重载:在同一个类中定义多个方法,它们具有相同的名字但参数列表不同。主要用于提供相同功能的不同实现。
- 重写:在子类中定义一个与父类方法具有相同签名的方法,以便提供子类的特定实现。且子类方法定义的访问修饰符,不能比父类更严格。例如父类方法是
protected
,那么子类方法不能是private
,但可以是public
。且子类方法抛出的异常必须与父类一致,或者是其父类异常的子类。主要用于实现运行时多态性。例如,子类对父类方法进行扩展或修改以适应特定需求。
为什么 Java 不支持多重继承?
主要是因为多继承会产生菱形继承(也叫钻石继承)问题,Java 之父就是吸取 C++ 他们的教训,因此在不支持多继承。
所谓的菱形继承很好理解,我们来看下这个图:
是不是看起来很像一个菱形,BC 继承了 A,然后 D 继承了 BC, 假设此时要调用 D 内定义在 A 的方法,因为 B 和 C 都有不同的实现,此时就会出现歧义,不知道应该调用哪个了。
this 和 super 是什么?有什么区别?
this
this关键字用于指代当前对象实例本身。在Java中,当你创建一个类的实例(对象)时,this就是指向这个实例的引用。
用法
- 引用当前对象的成员变量:当我们有一个局部变量与成员变量同名时,this可以帮助我们区分它们。
- 调用当前对象的成员方法:虽然直接调用成员方法很常见,但在某些特殊情况下(如构造方法中调用另一个构造方法),我们需要使用this。
super
super关键字用于从子类中访问父类的内容。当你创建一个子类对象时,它同时也包含了父类的所有内容(成员变量、成员方法等),super就是指向这个父类内容的引用。
用法
- 访问父类的成员变量:如果子类覆盖了父类的成员变量,我们可以使用super来访问父类的成员变量。
- 调用父类的成员方法:如果子类覆盖了父类的方法,我们可以使用super来调用父类的方法。
- 调用父类的构造方法:在子类的构造方法中,我们可以使用super()来调用父类的构造方法。
区别
- this引用的是当前对象本身,可以用来访问当前对象的成员变量、成员方法或返回当前对象本身。
- super引用的是当前对象的父类对象,可以用来访问父类的成员变量、成员方法或构造方法。
- 在构造方法中,this(…)用于调用当前类的其他构造方法,而super(…)用于调用父类的构造方法。注意,这两者在构造方法中必须是第一条语句。
- this和super都不能在静态方法、静态代码块或类变量声明中使用,因为它们都依赖于具体的对象实例。
接口和抽象类有什么区别?
接口:只能包含抽象方法(但在 Java8 之后可以设置 default 方法或者静态方法),成员变量只能是 public static final
类型,当 like-a 的情况下用接口。
抽象类:可以包含成员变量和一般方法和抽象方法,当 is-a 并且主要用于代码复用的场景下使用抽象类继承的方式,子类必须实现抽象类中的抽象方法。
is-a、has-a、like-a 是什么?其怎么应用?
is-a、has-a、like-a 是什么?
- is-a :是一个,代表继承关系,如果 A is-a B,那么 B 就是 A 的父类。
- has-a :有一个,代表从属关系,如果A has a B,那么 B 就是 A 的组成部分。
- like-a :像一个,代表组合关系,如果 A like a B,那么 B 就是 A 的接口。
is-a、has-a、like-a 的应用
- 如果你确定两件对象之间是 is-a 的关系,那么此时你应该使用继承;比如菱形、圆形和方形都是形状的一种,那么他们都应该从形状类继承。
- 如果你确定两件对象之间是 has-a 的关系,那么此时你应该使用聚合;比如电脑是由显示器、CPU、硬盘等组成的,那么你应该把显示器、CPU、硬盘这些类聚合成电脑类。
- 如果你确定两件对象之间是 like-a 的关系,那么此时你应该使用组合;比如空调继承于制冷机,但它同时有加热功能,那么你应该把让空调继承制冷机类,并实现加热接口。
Java 中的序列化和反序列化是什么?
序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。Java 序列化不包含静态变量。
反序列化就是将字节序列格式转换为对象的过程。
什么是 Java 内部类?它有什么作用?
内部类顾名思义就是定义在一个类的内部的类。它主要作用是为了封装和逻辑分组,提供更清晰的代码组织结构。
通过内部类,可以把逻辑上相关的类组织在一起,提升封装性和代码的可读性。后期维护时都在一个类里面,不需要在各地方找来找去。
按位置分:在成员变量的位置定义,则是成员内部类,在方法内定义,则是局部内部类。
如果用 static 修饰则为静态内部类,最后还有匿名内部类。
1)成员内部类,定义在另一个类中的类,可以使用外部类的所有成员变量以及方法,包括 private 的。
2)静态内部类,只能访问外部类的静态成员变量以及方法,其实它就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间。
3)局部内部类,指在方法中定义的类,只在该方法内可见,可以访问外部类的成员以及方法中的局部变量(需要声明为 final 或 effectively final)。
4)匿名类,指的是没有类名的内部类。用于简化实现接口和继承类的代码,仅在创建对象时使用,例如回调逻辑定义场景。
局部内部类用的比较少,常用成员内部类、静态内部类和匿名内部类。
实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和外部类没有任何区别,所以在 JVM 中是没有内部类的概念的。
Java 泛型的作用是什么?什么是泛型擦除?
泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。
并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。
泛型擦除指的是参数类型在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。(在代码里写死的泛型类型是不会被擦除的)。例如,在运行时无法直接通过instanceof
判断一个对象是否属于某个具体的泛型类型。
其他小知识
虽然Java中的泛型存在类型擦除,但可以通过反射来创建泛型数组。例如,可以先获取数组的Class
对象,然后利用Array.newInstance
方法来创建泛型数组。
反射可以绕过泛型类型检查。因为泛型类型检查主要在编译时进行,而反射是在运行时操作类和对象的机制,通过反射可以访问和操作那些在编译时受泛型约束的方法和类,即使传入不符合泛型约束的参数类型,也能在运行时通过反射调用相关方法。
什么是 Java 泛型的上下界限定符?
上界限定符是 extends ,下界限定符是 super
<? extends T>
表示类型的上界,?这个类型要么是 T ,要么是 T 的子类
<? super T>
表示类型的下界(也叫做超类型限定),?这个类型是 T 的超类型(父类型),直至 Object
我们在使用上下界通配符的时候,需要遵循 pecs 原则,即Producer Extends, Consumer Super;上界生产,下界消费。
什么意思呢?
如果要从集合中读取类型 T 的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends),如上面的 processNumber
方法,我们是要从方法中得到 T 类型,也就是方法给我们生产。
如果要从集合中写入类型 T 的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super),如上面的 addElements
方法,我们是要往方法中传入 T 类型,也就是方法帮我们消费。
你使用过 Java 的反射机制吗?如何应用反射?
反射其实就是 Java 提供的能在运行期得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。
一般在业务编码中不会用到反射,在框架上用的较多,因为很多场景需要很灵活,不确定目标对象的类型,届时只能通过反射动态获取对象信息。
例如 Spring 使用反射机制来读取和解析配置文件,从而实现依赖注入和面向切面编程等功能。
比如动态代理场景可以使用反射机制在运行时动态地创建代理对象。
所以反射机制的优点是:
- 可以动态地获取类的信息,不需要在编译时就知道类的信息。
- 可以动态地创建对象,不需要在编译时就知道对象的类型。
- 可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。
虽然反射很灵活,但是它有个明显的缺点,性能问题。
如果正常调用没影响,但是在高并发场景下就一点性能问题就会放大。
之所以反射有性能问题是因为反射是在运行时进行的,所以程序每次反射解析检查方法的类型等都需要从 class 的类信息加载进行运行时的动态检查。
所以 Apache BeanUtils 的 copy 在高并发下就有性能问题。
如何优化呢?
缓存,例如把第一次得到的 Method 缓存起来,后续就不需要再调用 Class.getDeclaredMethod 也就不需要再次动态加载了,这样就可以避免反射性能问题。
Java 中的注解原理是什么?
注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。
有了标记之后,我们就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。
比如我们可以定义一些切面,在执行一些方法的时候看下方法上是否有某个注解标记,如果是的话可以执行一些特殊逻辑(RUNTIME类型的注解)。
注解生命周期有三大类,分别是:
- RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
- RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
- RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
所以我上文写的是解析的时候,没写具体是解析啥,因为不同的生命周期的解析动作是不同的。
比如 @Override
就是给编译器用的,编译器编译的时候检查没问题就over了,class文件里面不会有 Override 这个标记。
再比如 Spring 常见的 @Autowired
,就是 RUNTIME 的,所以在运行的时候可以通过反射得到注解的信息,还能拿到标记的值 required 。
所以注解就是一个标记,可以给编译器用、也能运行时候用。
什么是 Java 的 SPI(Service Provider Interface)机制?
SPI(Service Provider Interface)服务提供接口是 Java 的机制,主要用于实现模块化开发和插件化扩展。 SPI 机制允许服务提供者通过特定的配置文件将自己的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。
一个典型的 SPI 应用场景是 JDBC(Java 数据库连接库),不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。
此外,我们使用的主流 Java 开发框架中,几乎都使用到了 SPI 机制,比如 Servlet 容器、日志框架、ORM 框架、Spring 框架。所以这是 Java 开发者必须掌握的一个重要特性!
如何实现 SPI?
分为系统实现和自定义实现。
系统实现
其实 Java 内已经提供了 SPI 机制相关的 API 接口,可以直接使用,这种方式最简单。
- 首先在 resources 资源目录下创建 META-INF/services 目录,并且创建一个名称为要实现的接口的空文件
- 在文件中填写自己定制的接口实现类的 完整类路径,如图:
- 直接使用系统内置的 ServiceLoader 动态加载指定接口的实现类,代码如下:
// 指定序列化器 |
上述代码能够获取到所有文件中编写的实现类对象,选择一个使用即可。
自定义 SPI 实现
系统实现 SPI 虽然简单,但是如果我们想定制多个不同的接口实现类,就没办法在框架中指定使用哪一个了,也就无法实现我们 “通过配置快速指定序列化器” 的需求。
所以我们需要自己定义 SPI 机制的实现,只要能够根据配置加载到类即可。
比如读取如下配置文件,能够得到一个 序列化器名称 => 序列化器实现类对象 的映射,之后就可以根据用户配置的序列化器名称动态加载指定实现类对象了
jdk=com.yupi.yurpc.serializer.JdkSerializer |
什么是 Java 的网络编程?
这题一般会出现在笔试题中,例如让你手写一个基于 Java 实现网络通信的代码。
Java 的网络编程主要利用 java.net 包,它提供了用于网络通信的基本类和接口。
Java 网络编程的核心类:
- Socket:用于创建客户端套接字。
- ServerSocket:用于创建服务器套接字。
- DatagramSocket:用于创建支持 UDP 协议的套接字。
- URL:用于处理统一资源定位符。
- URLConnection:用于读取和写入 URL 引用的资源。
示例代码参考(以下代码时基于 TCP 通信的,一般笔试考察的都是 TCP):
服务端代码
import java.io.*; |
客户端代码
import java.io.*; |