JAVA基础之二-面向对象简述
java基础之二 - 面向对象简述
一、概述
如果有机会多接触几种语言,对于程序员多少是有好处的,至少有助于理解代码的运行真谛。
高级语言有很多是面向对象的,因为面向对象的优点是显而易见的。这里比较知名的有rust,java,c++,c#
但也有很多语言是面向过程的,鼎鼎有名有C,还有现在大家不太熟悉的pascal等。
无论是面向对象还是面向过程,都有自身的优点,这个优点主要是工程上优点,而不是性能、安全上的。
工程优点的意思就是:节约成本-要么更容易开发,要么更有利于维护。
不过很多时候,面向过程的程序可能会比面向对象的程序快,这是因为面向过程偏向于解决具体问题,而面向对象偏向于一种模式。
具体到技术细节,此处略,因为内容太多,非本文关注的。
不过面向过程可以理解为绿色通道,而面向对象就是一个个关卡构成的路。绿通肯定比关卡重重来的快。(注意,这仅仅是效果比喻,非结构比喻)
在工程上,如果你制造一个灵活多变,功能强的东西,通常意味着更高的成本。如果制造了一个专用的,往往意味着专用,高效(某个方面)。
例如瑞士军刀对很多人而言就是个鸡肋,而水果刀就能更好地切削水果。
最后,强调下,这仅仅是面向过程相对面向对象的一个大体优势,具体则取决于工程师的水平,具体的编译器等。
随着各个语言的不断完善,它们会添加一些新的规范/技术来实现面向对象/面向过程的优点,并尽量克服自身的劣势。
如果我们仔细研究各自的发展历史,就可以验证这一点。但它们终究没有变成对方,是因为各自有不可替代的优势。
JAVA是一种面向对象的语言,但是和C++之类的面向对象又不太一样,主要是因为JAVA要求所有类强制继承于Object。
二、面向对象的核心优点
2.1经典优势
前文已经说过,在性能上,面向对象没有什么优点(至少目前是这样的),所以这里主要说工程优点。
封装
意味着减少了耦合,更少的耦合意味着更容易复用、扩展,也更容易设计。
这是因为设计一个类的时候只需要专注于对外暴露什么即可,其它的不用关心。当类设计的简单,那么无论是自身还是其它类的设计
都变得简单。类和类之间只需要遵循约定即可替换(这有助于维护和扩展)
继承与多态
增强了代码重用,降低了编码工作量。
重载
减低了大脑负担,简化了命名,同时也让熟悉这些内容变得相对简单。这是重载的核心优势。不过在其它非oop语言中,重载也基本是可行的。
简而言之,面向对象的核心优势就是:代码复用,增加扩展。降低开发成本,降低维护成本。
关于这个经典优势,很多书本/资料给的例子都是关于画笔的,这个例子用在这里的确非常合适。
三、面向对象的缺点
以下是通过文心一言收集的缺点,句句在理:
1.复杂性增加
设计和实现复杂度:面向对象设计需要更多的时间和精力来定义类、接口、继承关系、多态等概念。对于小型或简单的项目,这种额外的复杂度可能并不值得。
学习曲线:对于初学者来说,面向对象编程的概念(如封装、继承、多态等)可能比较抽象和难以理解,需要较长的学习曲线。
2.性能开销
运行时开销:面向对象编程中的动态绑定、继承和多态等特性可能会导致运行时性能下降。例如,动态绑定需要在运行时解析方法调用,这可能比静态绑定(即直接调用函数)更耗时。
内存使用:面向对象编程中的对象需要额外的内存来存储对象的元数据(如类型信息、方法表等),这可能会增加内存的使用量。
3.过度设计
设计模式滥用:面向对象编程鼓励使用设计模式来解决问题,但有时候这些设计模式可能会被过度使用或滥用,导致代码变得复杂而难以理解。
过度抽象:为了追求高度的抽象和封装,可能会创建过多的类和接口,使得系统的架构变得庞大而复杂,难以维护。
4.测试难度
依赖关系:面向对象编程中的对象之间可能存在复杂的依赖关系,这使得测试变得更加困难。特别是在进行单元测试时,需要模拟或隔离这些依赖关系,这可能会增加测试的复杂性和成本。
状态管理:面向对象编程中的对象通常具有状态,这增加了测试的难度。需要确保在测试过程中对象的状态符合预期,以避免状态不一致导致的测试失败。
5.不适合所有问题
简单问题复杂化:对于一些简单的问题,使用面向对象编程可能会将问题复杂化。例如,对于一些简单的脚本或工具,使用函数式编程或过程式编程可能更加直观和高效。
数据密集型应用:对于数据密集型应用(如大数据处理、机器学习等),面向对象编程可能不是最佳选择。这些应用可能更适合使用更底层、更直接的编程范式(如函数式编程或数据并行处理)。
如果在选择语言之前,以上劣势的确需要考虑。
但是当我们没有选择的时候(如果只会java或者必须用java开发),那么主要的问题在于:
首先是过度设计,其次是性能开销
现在的很多代码生成就有过度设计的典型问题:生成了过多的类
例如一个CRUD,在后端要包含:
- 用于dao得Mapper
- 一个业务接口,一个业务接口实现类
- 一个控制器
- 一个实体类
- 有的时候还要创建各种O,用于传参、展示
虽然这样也有工程上的优点,但是从性能出发而言,基本是没有什么优点。
不怕笑话,如果直接在控制器中用jdbc访问并实现一些逻辑,就能实现一个功能。
虽然我本人不会这么做,但那样的确对于性能有不少帮助。
当然这过度设计的一个小方面,不是过度设计的主要表现:过度的继承、把对象拆分得过于细碎。
如果是按照套路做CRUD,问题不大,因为CRUD基本是代码生成工具生成的,就那样吧。
但是有些系统做复杂了,还是很有一些不简单的CRUD代码。这个时候如何设计类就会有一定的难度,难度就在于如何避免过度设计。
使用面向对象,很容易让工程师动不动就创建新的类。 当然这是所有面向对象语言的通病,是天然的伴生品。
至于性能开销,是在java这个框架下的有限优化,可以努力一把,不过不要太费劲。当然这不是说代码乱写。
写CRUD程序的时候,性能更多时候和JAVA没有什么显著关系(在遵守一些基本要求的前提下),架构师、设计师、dba的作用更加显著。
所以,这也意味着,对大部分工程师是一个利好:毕竟不要费劲心思考虑安全性、性能等等,门槛低了。
如要考虑真实的性能,最好改用其它语言,例如rust,或者jcp考虑使用多平台编译器,并废弃JVM,就像C,C++那样。
阿里的app如果真要考虑性能,早就应该考虑用c,c++之类的语言改写了。
注意:这里性能应该是相对于系统软件、图形、视频处理等软件要求的性能。 应用软件的性能只要不乱写,主要性能大部分取决于数据库。
四、java面向对象的特点(J8前就有)
最大的不同(和C++比较)-不支持多继承
因为不支持多继承,那么一个新的类想利用已有类的能力的时候,只有利用组合模式等方式来实现。
以设计瑞士军刀为例子,假设一把刀山有三个组件:螺丝刀、开瓶器、小刀
现在系统中已经存在了Screwdriver,Bottleopener,Knife
现在要设计Swtool。
实现方式一:新类添加相关类类型的属性(经典组合)
public class Swtool{ private Screwdriver driver; private Bottleopener opener; private Knife knife; .... }
这是最简单粗暴的
实现方式二:在实现这个功能的方法中创建对应类示例
public class Swtool{ public void openBottle(){ Bottleopener opener=new Bottleopener(); opener.do(); } }
其它....
还有更复杂的,例如静态代理,动态代理之类的。
但不管哪一种,工程角度看,好像都没有那么优雅,时间也要浪费一些。
五、J8及之后和类有关的新特性
https://blog.csdn.net/u022812849/article/details/138213697
注意:某些特性可能J8之前就有,记忆不是太清楚了。
5.1、J8对接口的改造
默认方法
目的:减少重复的代码。因为有一定的概率各个实现类所实现体是一样,或者大部分是一样,这个时候default方法就有好处。
静态方法
目的:工具化接口,使得接口直接具有工具类的作用,作用等同于类的公共静态方法
由于是公共静态的,所以,它可以在任何地方被调用
5.2、J9对接口的改造
私有方法 (默认私有方法)
目的:强化默认方法的功能,减少重复代码,只能被默认方法调用
静态私有方法
目的:私有方法的增强,可以被其它三类接口方法使用(公共抽象的例外,因为只能实例化,实例化后就无法访问了),但也仅限于在接口中。
减少重复代码。
java接口对各种类型方法的支持,虽然增强了灵活性,但是也让接口的特点没有那么鲜明,某些方面和抽象类过于类似。
我本人有时候会犹豫用接口来设计类还是用抽象类来设计接口,关于这个本后面有专门的章节比较接口和抽象类。
我个人怀疑这种演化的必要性。 在不少的高级语言中,就没有接口这这种东西,也能活得好好的。把接口、抽象类、类搞得这么相似,会不会有什么问题?
5.3、J16~17对类的改造
5.3.1、record-记录
这是一个极好用的东西,java把它搞得和javascript中的对象类似,似乎在什么地方都可以定义。
record在效果上类似pojo,但比pojo方便多了。一个是代码少,其次引用属性也更加自然,不要动不动就get,set,更好读一些。
预计在更新的版本中,可以直接通过点语法类访问属性。
这个record和pascal/delphi的record有点相似。
特性:
提高代码可读性和清晰度促进不变性设计,降低风险
降低编码量,提升效率
不支持继承其他类或接口(除了隐式继承的 java.lang.Record 接口)
也就是说,record的出现主要是为了工程目的,而不是出于性能、安全等目的。
由于我们一般不会在record添加方法(可以添加),有些时候还是有一些些不容易察觉的性能优势,但基本可以忽略不计。
示例
package study.base.oop.record; /** * 公共可见 */ public record TranMessage(String name, String mType, String lvl, String content) { public String decode() { String newMsg=content.substring(3); return newMsg; } } // Human类接接收record类型 package study.base.oop.record; public class Human extends Animal { private String gender; private Date birthDay; private Integer power; public void sendMessage(TranMessage msg) { System.out.println(msg.content()); } public void readMessage(TranMessage msg) { System.out.println(msg.decode()); } public void readBook() { record Book( String name,String author,BigDecimal price,String content ) {}; Book lunyu=new Book("论语","孔子",new BigDecimal("100"),"学而不思则罔.."); Book zz=new Book("左传","左丘明",new BigDecimal("100.908"),"肉食者鄙,未能远谋..."); System.out.println(lunyu.toString()); System.out.println(zz.toString()); } public static void main (String[] args) { Human h=new Human(); h.readBook(); } }
这里演示了record的
- 定义:可以单独定义(包可见或者public可见),也可以作为内部类存在;也可以在方法内部定义。定义也非常简单类似方法参数
但还是不够方便,还可以继续优化
- 书写属性-直接 xxx.***(), 这里xxx是变量名称,***指的是属性名称。例如上文 msg.content()
个人觉得还可以继续优化如下:
//定义 -- 可以省掉 {} public record Message(String title,String content); //使用 -- 不要用方法访问,而是直接属性访问 record msg=new Message("通知","大家赶紧上学去"); String allWord=msg.title+msg.content;
由于record推出不是很久,所以各个框架对它的支持并不完善。
record的序列和反序列等都没有什么问题,但是在spring 6.1.10中,jdbcTemplate对于record的支持是存在不足的。
当执行以下代码的时候:
RowMapper<JobSumResult> rm = new BeanPropertyRowMapper<>(JobSumResult.class); JobSumResult temp = this.nJdbTp.queryForObject(sql, args, rm); //其中 JobSumResult 是record类型 private record JobSumResult(Timestamp startTime, Timestamp endTime, BigInteger avgEmsecs, BigInteger maxEmsecs, BigInteger minEmsecs, Integer aqy, Integer succQty, Integer failQty) { };
spring会提示:org.springframework.beans.BeanInstantiationException: Failed to instantiate [....$JobSumResult]: No default constructor found
这个明显是spring的问题,不是record的问题。Spring的jdbcTemplate在反射的时候总是会试图找没有参数的构造函数,不幸的是record只有一个默认构造函数,
这个构造函数的参数同成员,不是无参的,所以不满足要求,所以会报告异常。
如果把上例的JobSumResult修改为private static class,则不会有这个问题,这是因为每个普通的class的如果你不写构造函数,那么会有一个没有参数的默认构造函数,而那
正式jdbcTemplate需要的。如果你为JobSumResult创建一个带参数的构造函数,同时不具有无参的构造函数,也是会失败的。
有许多工具,它们反射的时候,必须基于特定类型的对象才可以,可能这也是Spring代码中存在大量的内部静态类的缘故了。
不知在现有的6.1.13或者更高版本中是否已经解决这个问题。
最后,一句话,record大有可为!
5.3.2、非静态内部类定义静态成员
按照这种趋势,越搞越复杂,虽然某个时刻是方便了。
搞得太复杂,老是让我想起两个东西:
- 简单的美
- 卡拉什尼科夫自动步枪(AK47)
我觉得,JAVA的进化应该考虑功能效率,性能和安全,不要追求过于边缘的东西。
5.3.3、密封类
注:这一段直接由文心一言生成.
Java的密封类(Sealed Classes)是Java 15中引入的一个预览功能,并在Java 17中成为正式功能。这一特性主要用于增强类的封装性和模块化,限制哪些类可以继承给定的类。
密封类提供了一种机制,使得开发者能够更精确地控制类的继承层次结构,从而避免意外扩展和可能的错误。
密封类的主要用途和优点包括:
提高封装性:通过限制哪些类可以继承一个给定的类,可以增加类的封装性。这有助于防止类的滥用,确保只有预期的子类才能被创建和使用。
增强模块性:在模块化编程中,密封类可以确保模块之间的依赖关系更加清晰和可控。通过限制哪些模块可以扩展某个类,可以减少模块之间的耦合度,提高系统的可维护性和可扩展性。
促进类型安全:通过使用密封类和接口,可以确保在类型系统中不会出现意外的类型。这有助于在编译时捕获错误,而不是在运行时,从而提高代码的可靠性和可预测性。
优化性能:在某些情况下,密封类可以用于优化JVM的性能。例如,如果JVM知道一个类的所有可能子类,它可以使用更高效的算法来处理多态调用和类型检查。
密封类的基本用法包括:
使用sealed关键字声明一个密封类或接口。
在声明时,指定哪些类可以继承这个密封类或实现这个密封接口(使用permits关键字)。
如果一个类继承了一个密封类,或者一个接口实现了一个密封接口,并且这个类或接口不在permits子句中列出,那么编译器会报错。
基本同意。密封类的主要作用是工程上的,还有一点点性能上的好处。
这是一个新东西,和final修饰符的作用暂时没有那么明显的区分。
定义示例:
package study.base.oop.classes.modifier; public sealed class SealedMan permits SonOfSealedMan { public void think3TimesOneDay() { } } package study.base.oop.classes.modifier; public non-sealed class SonOfSealedMan extends SealedMan { }
在上例中,封装类的儿子只能是三种之一:依然是封装类、非封装类(一定要加上non-sealed)、最终类
5.3.4、隐藏类
本人没有用过。毕竟这是JAVA15的特性,而本人直接从J8跳到J17的.
找了不少的参考资料,发现这个东西仅仅用于做框架用的。
这意味着如果做一般的应用开发,不要过多去考虑。
参考:Java 15中的隐藏类(Hidden Classes) | Baeldung中文网 (baeldung-cn.com)
老美这个文章介绍的很清楚了,总结下:
1.作用-搭建架构为主 ,当然如果坚持要用于一般的应用也可以
2.使用途径-定义,编译,然后通过反射生成,再调用
3.创建隐藏类的关键-不在于定义,而是在反射的时候
Class<?> hiddenClass = lookup.defineHiddenClass(IOUtils.toByteArray(stream), true, ClassOption.NESTMATE).lookupClass();
4.lambda表达式会用到这个功能
老美的文章还提供了和匿名类的比较。
其余略,个人没有用过,就不评价了。
5.4、各种奇怪怪怪的类和名词
java随着发展,创造了许多的类名词。
下表是比较基本的类名词
序号 | 英文名称 | 名称 | 关键字/修饰符 | 使用场景 | 注意事项 | 备注 |
1 | class | 类 | 任何地方 | 无 | ||
2 | abstract class | 抽象类 | abstract |
需要完成抽象工作,构建较复杂的、 提升复用性的 |
无 | |
3 | sealed class | 密封类 | sealed | 限定继承关系的,简化维护工作的 | ||
4 | hidden class | 隐藏类 | 框架,例如lambda | 用于框架 | ||
5 |
注:从面向对象的角度而言,普通类和抽象类是根本,其它各种仅仅是为了工程方便而存在的,当然如果把抽象类也那么理解也可以。
这些类和结合上修饰符和作用范围,可以创造出许许多多的的名词。
为了便于行文,不论什么关键字,除了class,其它的都称为修饰符:
根据不同分类,有如下修饰符:
- 可见范围-public,package,private,protected,final
- 是否抽象-abstract
- 继承范围-sealed ,non-sealed
事实上,可见范围,也影响着继承。
记住这些名词和基本内容,在设计的时候,再验证可以如何组合即可。
幸运的时候,现代ide可以在编码阶段就能避免这个问题。
5.3、各种内部类简述
实际上,java允许写各种千奇百怪的内部类。
按照内部类的位置,可以划分为:
- 类外内部类-这些类和主类同个文件,但是不包含在主类内。不能使用public修饰符,意味着这些类外内部类不能被外部访问
- 类内内部类-这些类被包含在主类中,可以有各种修饰符。如果使用public,也能被外部实例访问,不过创建上稍微麻烦一些
- 方法内部类-主要定义在方法中,只能在方法内部使用。其它方法,其它类实例是无法使用这些的。
a.以上三种位置的,只有类内内部类是可以被其它类的实例访问的,另外两种都不行
b.类外内部类和类内内部类的主要区别之二:前者不能被儿孙继承
总的来说 ,内部类就是为避免工程上对外暴露不好看,外部不必要看的内容。当然如果想的话,有可以添加public修饰符。
定义在哪里,对系统的性能和安全基本没有啥影响,主要出于工程的目的-眼不见为净!!!
在spring的代码中,不乏使用内部类的情况。
但我个人并不喜欢过多使用内部类,除非这个内部类不为了继承用。
偶尔也会用上,主要是为了让代码好看一些,不想让主类变得过于臃肿,但同时也不希望其它功能使用这些特定的功能,所以就可能会定义
一些内部类来用。
内部类一旦考虑到继承,就变得有点混乱。
以下的例子,演示了让人烦乱的内部类及其继承:
package study.base.oop.classes.inner; /** * 令人目瞪口呆的各种内部类 */ public class InnerMan { public void work() { final class Tool{ void show() { System.out.println("拔出我的家伙..."); } } class Target{ void show() { System.out.println("这是一个必须完成的任务"); } } record Job(Tool tool,Target target) {}; class JobDetail{ static void doJob(Job job) { job.tool.show(); job.target.show(); } } Job job=new Job(new Tool(),new Target()); JobDetail.doJob(job); } static class Head{ Integer weight; Head(Integer weight){ this.weight=weight; } } /** * 爱好 */ public abstract class Fav{ } /** * 活 */ sealed class Work permits HomeWork,InnerFoodWork{ } final class FoodFav extends Fav{ } final class HomeWork extends Work{ } public non-sealed class InnerFoodWork extends Work{ } } /** * 定义在同个文件的其它类,不能使用public修饰符,意味着这些类外内部类不能被外部访问 */ /** * 爱好 */ abstract class OuterFav{ } /** * 活 */ sealed class Work permits HomeWork,OuterFoodWork{ } final class FoodFav extends OuterFav{ } final class HomeWork extends Work{ } non-sealed class OuterFoodWork extends Work{ }
InnerMan的子类:
package study.base.oop.classes.inner; /** * 孩子如何使用父亲的内部类? * * 内部用内部/外部,外部用外部 */ public class SonOfInnerMan extends InnerMan { public void learn() { //可以实例化父亲的类内部类 Head head=new Head(67); } //内部继承内部 class SonHead extends Head{ SonHead(Integer weight) { super(weight); } } //内部继承外部 final class SonFoodWork extends OuterFoodWork{ } } //外部继承外部 final class SonFoodWork extends OuterFav{ }
考虑到17中可以使用record,我会尝试多用一些内部类,反正应用类型的软件对性能不会有什么苛刻的要求。
六、比较抽象类和接口
接口有什么用? 应该是为了模拟多继承,其次就是为了便于模块化,并对外只暴露必须的内容。
如果没有接口,那么要做模块开发就会变得麻烦。
有了接口,实现多态很容易,插件开发等等也容易。
更具体的见网友总结:
特性 | 抽象类 | 接口 |
---|---|---|
定义 | 一种不能被实例化的类,可以包含抽象方法和具体方法,以及成员变量(包括常量和普通变量) | 一种完全抽象的类型,只能定义方法的签名(即方法的返回类型、方法名和参数列表),不能包含方法的实现,只能包含常量(即被`final`修饰的成员变量) |
实现方式 | 抽象类可以有部分方法的实现,也可以完全没有实现(仅包含抽象方法) | 接口只能定义方法的签名,不能有方法的实现 |
继承关系 | 一个类只能继承一个抽象类(Java不支持多继承类) | 一个类可以实现多个接口,实现多重继承的效果 |
构造函数 | 抽象类可以有构造函数,用于初始化抽象类的成员变量,但构造函数不能被实例化 | 接口不能有构造函数,因为接口不能被实例化 |
方法修饰符 | 抽象类中的方法可以使用`public`、`protected`、`default`(包级私有)、`private`等访问修饰符,但抽象方法通常使用`public`或`protected` | 接口中的方法默认为`public`,且不能用其他修饰符(如`private`、`protected`、`default`)修饰 |
默认实现 | 抽象类可以包含具体方法的实现,子类可以选择继承这些实现或重写它们 | 接口中的方法都是抽象的,没有默认实现,实现接口的类必须提供所有方法的具体实现 |
静态方法 | 抽象类中可以定义静态方法,这些方法属于类本身,而不是类的实例 | 接口中不能定义静态方法(从Java 8开始,接口中可以通过默认方法或静态方法提供实现,但这里主要讨论传统意义上的接口) |
使用场景 | 适用于定义类的结构,提供一些共同的属性和方法,可以作为多个子类的父类。当需要提供一些方法的默认实现时,使用抽象类更为合适 |
适用于定义类的行为,定义一组相关的功能,实现类可以根据需要实现多个接口。当需要实现多重继承时,必须使用接口 适用于插件开发 |
注:上表是比较传统的抽象类和接口。 如果是j8之后的,就不太适用了,因为j8之后,接口已经变得复杂了。
随着java把接口搞得越发复杂,好像抽象类已经没有什么存在的意义了....
选择抽象类还是接口,还是有点小门道的,值得另外开篇说这个问题。 本文就先这样吧!
七、JAVA类开发体验
本人用过的语言比较多,大概有10来种。java算是比较久的,当前也以java为主。
总体而言,对java的类还算满意,就是希望不要把java变成javascript,也不要把类搞得过于复杂了,这些方向可能不利于java的发展。
工作的时候,大部分时候都是一些CRUD机械行活,没有挑战性,只有偶尔设计一些复杂一些东西的时候才会考虑用上类的各种特性,包括继承、封装、多态等等。
java的接口也还可以,虽然这不是java独有的。
八、小结
java能够实现面向对象编程的基本目标:抽象、封装、继承、多态
这些特性的实现,对于实现工程目标(复用、扩展、适当的安全)还是有用的。
和其它OOP语言一样,JAVA同样存在有天然的缺陷:过度设计、性能劣势
JVM的存在,让java语言变得更慢。所以现在只要优化JVM(程序员只要重新编译下即可),就能让有关JAVA程序获得显著的提升。某些方面可以说前期JVM设计还是很不上心的。
至于不能适用于所有场景、复杂化、不利于测试,每种OOP语言都有这样的问题。
随着java的快速推进,近年java和类(含接口)有关的特性让人有点目不暇接。
有点复杂化倾向,可能不是太好,例如隐藏类。
record的出现绝对是一个好事,不过值得再简化下。
最后java的接口现在变得复杂且让人印象深刻,而内部类则有点让人望而生畏,不过适当使用好像也还可以!