为什么建议多用组合少用继承?

典型回答 作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。 复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。 继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;继承是一种is-a关系。如狗是一种动物,特斯拉是一种车 组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系。如狗有一个尾巴,特斯拉有轮子 组合与继承的区别和联系 在继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;) 继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。) 组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法) 组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。 优缺点对比 组 合 关 系 继 承 关 系 优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 优点:具有较好的可扩展性 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 缺点:不支持动态继承。在运行时,子类无法选择不同的父类 优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 缺点:子类不能改变父类的接口 缺点:整体类不能自动获得和局部类同样的接口 优点:子类能自动继承父类的接口 缺点:创建整体类的对象时,需要创建所有局部类的对象 优点:创建子类的对象时,无须创建父类的对象 如何选择 相信很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。 ...

March 22, 2026 · 1 min · santu

什么是AIO、BIO和NIO?

典型回答 BIO (Blocking I/O):同步阻塞I/O,是JDK1.4之前的传统IO模型。 线程发起IO请求后,一直阻塞,直到缓冲区数据就绪后,再进入下一步操作。 NIO (Non-Blocking I/O):同步非阻塞IO,线程发起IO请求后,不需要阻塞,立即返回。用户线程不原地等待IO缓冲区,可以先做一些其他操作,只需要定时轮询检查IO缓冲区数据是否就绪即可。 AIO ( Asynchronous I/O):异步非阻塞I/O模型。线程发起IO请求后,不需要阻塞,立即返回,也不需要定时轮询检查结果,异步IO操作之后会回调通知调用方。 知识扩展 Java中BIO、NIO、AIO分别适用哪些场景? BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。 AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 同步,异步,阻塞,非阻塞的区别 同步、异步、阻塞、非阻塞怎么理解? 操作系统的IO模型有哪五种? 操作系统的IO模型有哪些?

March 22, 2026 · 1 min · santu

什么是UUID,能保证唯一吗?

典型回答 UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它的目标是保证对在同一时空中的所有机器都是唯一的。 UUID 的生成是基于一定算法,通常使用的是随机数生成器或者基于时间戳的方式,生成的 UUID 由 32 位 16 进制数表示,共有 128 位(标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),共32个字符) 由于 UUID 是由 MAC 地址、时间戳、随机数等信息生成的,因此 UUID 具有极高的唯一性,可以说是几乎不可能重复,但是在实际实现过程中,UUID有多种实现版本,他们的唯一性指标也不尽相同。 UUID在具体实现上,有多个版本,有基于时间的UUID V1,基于随机数的 UUID V4等。 Java中的java.util.UUID生成的UUID是V3和V4两种: 优缺点 UUID的优点就是他的性能比较高,不依赖网络,本地就可以生成,使用起来也比较简单。 但是他也有两个比较明显的缺点,那就是长度过长和没有任何含义。长度自然不必说,他有32位16进制数字。对于"550e8400-e29b-41d4-a716-446655440000"这个字符串来说,我想任何一个程序员都看不出其表达的含义。一旦使用它作为全局唯一标识,就意味着在日后的问题排查和开发调试过程中会遇到很大的困难。 各个版本实现 V1. 基于时间戳的UUID 基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。 但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址。 MAC地址是与设备硬件直接关联的唯一标识符。通过获取到同一个 MAC 地址生成的大量UUID,可以被恶意用户或第三方通过反向工程解析出MAC地址,进而获取到设备的物理位置或用户身份信息。 在某些情况下,如果MAC地址被泄露,它可能被用于针对特定设备的网络攻击。 V2. DCE(Distributed Computing Environment)安全的UUID 和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID,这个版本的UUID在实际中较少用到。 V3. 基于名称空间的UUID(MD5) 基于名称的UUID通过计算名称和名称空间的MD5散列值得到。 这个版本的UUID保证了:相同名称空间中不同名称生成的UUID的唯一性;不同名称空间中的UUID的唯一性;相同名称空间中相同名称的UUID重复生成得到的结果是相同的。 V4. 基于随机数的UUID 根据随机数,或者伪随机数生成UUID。该版本 UUID 采用随机数生成器生成,它可以保证生成的 UUID 具有极佳的唯一性。但是因为基于随机数的,所以,并不适合数据量特别大的场景。 V5. 基于名称空间的UUID(SHA1) 和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。 V6.基于时间戳 + MAC 地址 ...

March 22, 2026 · 1 min · santu

什么是泛型?有什么好处?

典型回答 Java泛型(generics) 是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。 泛型的好处有两个: 方便:可以提高代码的复用性。以List接口为例,我们可以将String、Integer等类型放入List中,如不用泛型,存放String类型要写一个List接口,存放Integer要写另外一个List接口,泛型可以很好的解决这个问题 安全:在泛型出之前,通过Object实现的类型转换需要在运行时检查,如果类型转换出错,程序直接GG,可能会带来毁灭性打击。而泛型的作用就是在编译时做类型检查,这无疑增加程序的安全性 知识扩展 泛型是如何实现的 Java中的泛型通过类型擦除的方式来实现,通俗点理解,就是通过语法糖的形式,在.java->.class转换的阶段,将List擦除调转为List的手段。换句话说,Java的泛型只在编译期,Jvm是不会感知到泛型的。 什么是类型擦除? 类型擦除的缺点有哪些? 泛型不可以重载 泛型异常类不可以多次catch 泛型类中的静态变量也只有一份,不会有多份 对泛型通配符的理解 ✅泛型中上下界限定符extends 和 super有什么区别? List<?>, List, List之间的区别 List<?> 是一个未知类型的List,而List 其实是任意类型的List。可以把List, List赋值给List<?>,却不能把List赋值给 List 可以把任何带参数的类型传递给原始类型List,但却不能把List赋值给List,因为会产生编译错误(不支持协变) List<?>由于不确定列表中元素的具体类型,因此只能从这种列表中读取数据,而不能往里面添加除了 null 之外的任何元素。 在泛型为Integer的ArrayList中存放一个String类型的对象 通过反射可以实现: 1 2 3 4 5 6 public void test() throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); Method method = list.getClass().getMethod("add", Object.class); method.invoke(list, "Java反射机制实例"); System.out.println(list.get(0)); } 对数组协变和泛型非协变的理解 所谓协变,可以简单理解为因为Object是String的父类,所以Object[]同样是String[]的父类,这种情况Java是允许的;但是对于泛型来说,List和List半毛钱关系都没有 ...

March 22, 2026 · 1 min · santu

什么是深拷贝和浅拷贝?

典型回答 在计算机内存中,每个对象都有一个地址,这个地址指向对象在内存中存储的位置。当我们使用变量引用一个对象时,实际上是将该对象的地址赋值给变量。因此,如果我们将一个对象复制到另一个变量中,实际上是将对象的地址复制到了这个变量中。 **浅拷贝是指将一个对象复制到另一个变量中,但是只复制对象的地址,而不是对象本身。也就是说,原始对象和复制对象实际上是共享同一个内存地址的。**因此,如果我们修改了复制对象中的属性或元素,原始对象中对应的属性或元素也会被修改。 在Java中,我们常用的各种BeanUtils基本也都是浅拷贝的。 适用场景:浅拷贝的好处就是性能比较好,他只需要做一个引用的地址复制即可。当我们希望不同的对象,如对象1和对象2共享部分数据的时候,可以使用浅拷贝。或者对于一些简单的对象,比如没有很复杂的对象嵌套时,就可以用浅拷贝。 **深拷贝是指将一个对象及其所有子对象都复制到另一个变量中,也就是说,它会创建一个全新的对象,并将原始对象中的所有属性或元素都复制到新的对象中。**因此,如果我们修改复制对象中的属性或元素,原始对象中对应的属性或元素不会受到影响。 比如我们有一个User类,然后他的定义如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 class User { private String name; private String password; private Address address; //省略构造函数和setter/getter } public class Address { private String province; private String city; private String area; //省略构造函数和setter/getter } 当我们基于User类的一个对象user1拷贝出一个新的对象user2的时候,不管怎么样,user1和user2都是两个不同的对象,他们的地址也不会一样。但是其中的成员变量Address的话可能就因为深浅拷贝的不同而呈现不同的现象了。 ...

March 22, 2026 · 2 min · santu

你知道fastjson的反序列化漏洞吗

典型回答 当我们使用fastjson进行序列化的时候,当一个类中包含了一个接口(或抽象类)的时候,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无法拿到原始类型。 那么为了解决这个问题,fastjson引入了AutoType,即在序列化的时候,把原始类型记录下来。 因为有了autoType功能,那么fastjson在对JSON字符串进行反序列化的时候,就会读取@type到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。 那么这个特性就可能被利用,攻击者自己构造一个JSON字符串,并且使用@type指定一个自己想要使用的攻击类库实现攻击。 举个例子,黑客比较常用的攻击类库是com.sun.rowset.JdbcRowSetImpl,这是sun官方提供的一个类库,这个类的dataSourceName支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。 而fastjson在反序列化时会调用目标类的setter方法,那么如果黑客在JdbcRowSetImpl的dataSourceName中设置了一个想要执行的命令,那么就会导致很严重的后果。 如通过以下方式定一个JSON串,即可实现远程命令执行(在早期版本中,新版本中JdbcRowSetImpl已经被加了黑名单) `{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}` 这就是所谓的远程命令执行漏洞,即利用漏洞入侵到目标服务器,通过服务器执行命令。 扩展知识 AutoType fastjson的主要功能就是将Java Bean序列化成JSON字符串,这样得到字符串之后就可以通过数据库等方式进行持久化了。 但是,fastjson在序列化以及反序列化的过程中并没有使用Java自带的序列化机制,而是自定义了一套机制。 其实,对于JSON框架来说,想要把一个Java对象转换成字符串,可以有两种选择: 1、基于属性 2、基于setter/getter 而我们所常用的JSON序列化框架中,FastJson和jackson在把对象序列化成json字符串的时候,是通过遍历出该类中的所有getter方法进行的。Gson并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成json。 假设我们有以下一个Java类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Store { private String name; private Fruit fruit; public String getName() { return name; } public void setName(String name) { this.name = name; } public Fruit getFruit() { return fruit; } public void setFruit(Fruit fruit) { this.fruit = fruit; } } interface Fruit { } class Apple implements Fruit { private BigDecimal price; //省略 setter/getter、toString等 } 当我们要对他进行序列化的时候,fastjson会扫描其中的getter方法,即找到getName和getFruit,这时候就会将name和fruit两个字段的值序列化到JSON字符串中。 ...

March 22, 2026 · 2 min · santu

Java和C++主要区别有哪些?各有哪些优缺点?

典型回答 Java和C++都是面向对象的语言,他们一个是编译型语言,一个是解释型语言。 ✅如何理解面向对象和面向过程? C++是编译型语言(首先将源代码编译生成机器码,再由机器运行机器码),执行速度快、效率高;依赖编译器、跨平台性差些。 Java是解释型语言(源代码不是直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。),执行速度慢、效率低;依赖解释器、跨平台性好。 ✅Java是编译型还是解释型? PS:也有人说Java是半编译、半解释型语言。Java 编译器(javac)先将java源程序编译成Java字节码(.class),JVM负责解释执行字节码文件。 二者更多的主要区别如下: Java C++ 跨平台 平台无关 平台有关 内存管理 自动 手动 参数传递方式 值传递 引用、指针、值传递 多继承 不支持 支持 系统资源的控制能力 弱 强 适合领域 企业级Web应用开发 系统编程、游戏开发等 C++是平台相关的,Java是平台无关的。 ✅Java是如何实现的平台无关? Java是自动内存管理和垃圾回收的,C++需要手动内存管理,支持析构函数,Java没有析构函数的概念。 ✅JVM有哪些垃圾回收算法? ✅新生代和老年代的垃圾回收器有何区别? C++支持指针,引用,传值调用 。Java只有值传递。 ✅Java是值传递还是引用传递? C++支持多重继承,包括虚拟继承 。Java只允许单继承,需要多继承的情况要使用接口。 ✅为什么Java不支持多继承? C++对所有的数字类型有标准的范围限制,但字节长度是跟具体实现相关的,同一个类型在不同操作系统可能长度不一样。Java在所有平台上对所有的基本类型都有标准的范围限制和字节长度。 C++除了一些比较少见的情况之外和C语言兼容 。 Java没有对任何之前的语言向前兼容。但在语法上受 C/C++ 的影响很大 C++允许直接调用本地的系统库 。 Java要通过JNI调用。 Java的优点是跨平台能力强,支持自动内存管理减少内存泄露风险。有大量的库和框架支持(特别是企业级应用开发),并且还有较强的社区支持和资源。 Java的缺点是性能不如C++,对系统资源的控制能力较弱。 C++的优点是性能高,控制能力强。可以直接操作内存和硬件的能力。适用于系统编程、游戏开发、实时系统。同时也有丰富的库和工具,特别是在图形和游戏领域。 C++的缺点是内存管理复杂,容易出错。跨平台开发困难。代码会比较复杂,学习曲线比较陡。

March 22, 2026 · 1 min · santu

如何理解面向对象和面向过程?

典型回答 面向过程把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。 我们在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。最典型的用法就是实现一个简单的算法,比如实现冒泡排序。 面向对象将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。 就是说,在进行面向对象编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。比如想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。 面向对象有封装、继承、多态三大基本特征,和单一职责原则、开放封闭原则、Liskov替换原则、依赖倒置原则和 接口隔离原则等五大基本原则。 知识扩展 面向对象的三大基本特征? 三大基本特征:封装、继承、多态。 封装 封装就是把现实世界中的客观事物抽象成一个Java类,然后在类中存放属性和方法。如封装一个汽车类,其中包含了发动机、轮胎 、底盘等属性,并且有启动、前进等方法。 继承 像现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。 如Dog类可以继承Animal类,继承过来嘴巴、颜色等属性, 吃东西、奔跑等行为。 多态 多态是指在父类中定义的方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个方法在父类及其各个子类中具有不同含义。 ✅如何理解Java中的多态? 继承和实现 在Java中,接口可以继承接口,抽象类可以实现接口,抽象类也可以继承具体类。普通类可以实现接口,普通类也可以继承抽象类和普通类。 Java支持多实现,但是只支持单继承。即一个类可以实现多个接口,但是不能继承多个类。 ...

March 22, 2026 · 3 min · santu

Java中有了基本类型为什么还需要包装类?

典型回答 Java中有8种基本数据类型,这些基本类型又都有对应的包装类。 分类 基本数据类型 包装类 长度 表示范围 布尔型 boolean Boolean / / 整型 byte Byte 1字节 -128 到 127 short Short 2字节 -32,768 到 32,767 int Integer 4字节 -2,147,483,648 到 2,147,483,647 long Long 8字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 字符型 char Character 2字节 Unicode字符集中的任何字符 浮点型 float Float 4字节 约 -3.4E38 到 3.4E38 double Double 8字节 约 -1.7E308 到 1.7E308 因为Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是Object类型。 为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。 知识扩展 基本类型和包装类型的区别 默认值不同,基本类型的默认值为0, false或\u0000等,包装类默认为null 初始化方式不同,一个需要new,一个不需要 存储方式不同,基本类型保存在栈上,包装类对象保存在堆上(成员变量的话,在不考虑JIT优化的栈上分配时,都是随着对象一起保存在堆上的) ✅简单介绍一下JIT优化技术? ...

March 22, 2026 · 2 min · santu

如何理解Java中的多态?

典型回答 多态的概念比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。 如果按照这个概念来定义的话,那么多态应该是一种运行期的状态。为了实现运行期的多态,或者说是动态绑定,需要满足三个条件: 有类继承或者接口实现。 子类要重写父类的方法。 父类的引用指向子类的对象。 简单来一段代码解释下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Parent{ public void call(){ sout("im Parent"); } } public class Son extends Parent{// 1.有类继承或者接口实现 public void call(){// 2.子类要重写父类的方法 sout("im Son"); } } public class Daughter extends Parent{// 1.有类继承或者接口实现 public void call(){// 2.子类要重写父类的方法 sout("im Daughter"); } } public class Test{ public static void main(String[] args){ Parent p = new Son(); //3.父类的引用指向子类的对象 Parent p1 = new Daughter(); //3.父类的引用指向子类的对象 } } 这样,就实现了多态,同样是Parent类的实例,p.call 调用的是Son类的实现、p1.call调用的是Daughter的实现。 ...

March 22, 2026 · 2 min · santu

留言给博主