当前位置: 首页 > article >正文

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大有可为!

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随着发展,创造了许多的类名词。

下表是比较基本的类名词

序号英文名称名称关键字/修饰符使用场景注意事项备注
1class任何地方
2abstract class抽象类abstract

需要完成抽象工作,构建较复杂的、

提升复用性的

3sealed class密封类sealed限定继承关系的,简化维护工作的
4hidden class隐藏类框架,例如lambda用于框架
5

注:从面向对象的角度而言,普通类和抽象类是根本,其它各种仅仅是为了工程方便而存在的,当然如果把抽象类也那么理解也可以。

这些类和结合上修饰符和作用范围,可以创造出许许多多的的名词。

为了便于行文,不论什么关键字,除了class,其它的都称为修饰符:

根据不同分类,有如下修饰符:

  • 可见范围-public,package,private,protected,final
  • 是否抽象-abstract
  • 继承范围-sealed ,non-sealed

事实上,可见范围,也影响着继承。

记住这些名词和基本内容,在设计的时候,再验证可以如何组合即可。

幸运的时候,现代ide可以在编码阶段就能避免这个问题。

5.3、各种内部类简述

实际上,java允许写各种千奇百怪的内部类。

按照内部类的位置,可以划分为:

  1. 类外内部类-这些类和主类同个文件,但是不包含在主类内。不能使用public修饰符,意味着这些类外内部类不能被外部访问
  2. 类内内部类-这些类被包含在主类中,可以有各种修饰符。如果使用public,也能被外部实例访问,不过创建上稍微麻烦一些
  3. 方法内部类-主要定义在方法中,只能在方法内部使用。其它方法,其它类实例是无法使用这些的。

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的接口现在变得复杂且让人印象深刻,而内部类则有点让人望而生畏,不过适当使用好像也还可以!


http://www.kler.cn/a/282093.html

相关文章:

  • Python蓝桥杯刷题1
  • 在arm64架构下, Ubuntu 18.04.5 LTS 用命令安装和卸载qt4、qt5
  • 深入理解 C++ 二叉树
  • AI 提示词(Prompt)入门 十:最佳实践|详细询问,提供细节!
  • Javascript——设计模式(一)
  • 【阅读记录-章节1】Build a Large Language Model (From Scratch)
  • UE5学习笔记16-游戏模式中的一些事件,如何改变网格体和摄像头的碰撞
  • MosaicML-面向生成式AI的机器学习平台
  • 仅利用一维数组实现等值线图效果(附完整代码)
  • TeamTalk消息服务器学习
  • Nuxt3入门:介绍、项目安装和了解视图(第一节)
  • 【Android】Glide模块工作原理
  • 2024最全网络安全工程师面试题(附答案),金九银十找工作必看!
  • CARLA Drone: 首个实现从不同空中视角进行单目3D目标检测,并提供数据集
  • 保证MQ的高可用性:RabbitMQ为例
  • 后端开发刷题 | 面试篇4
  • 合合信息acge模型获C-MTEB第一,文本向量化迎来新突破
  • Git 的基本使用
  • 【js】箭头函数和普通函数在this指向的区别
  • 深入理解DPO(Direct Preference Optimization)算法
  • MATLAB发票识别系统
  • 【Material-UI】Rating组件中的Rating precision属性
  • 31套科技风PPT模版免费下载
  • 电商云账户:空中分账场景的优势探索
  • [动态规划]---背包问题
  • 七、Centos安装LDAP--Docker版--已失败