Java高级笔记(一)

编码的艺术笔记

1.1 编码的价值

  • 双重价值:代码不仅用于指导计算机执行特定任务以完成软件功能,同时也作为文档告知未来的代码修改者(包括原作者)其意图和功能。
  • 综合认识观:代码是驱动计算机运行的指令序列。每个指令都有语义,指令组合后的功能应与这些语义一致。编码过程就是将程序员理解的语义翻译成计算机指令的过程。如果翻译后的代码在概念、结构上与语义相近,则更能直接体现程序员的意图,并具有更高的可读性。

1.2 概念与命名

1.2.1 名副其实的功能描述

  • 代码中的自然语义应该与其逻辑所体现的语义保持一致,使代码更加直观易懂。

1.2.2 有意义的区分

  • 命名需要能够区分相同场景中不同类型或用途的对象。例如,在结婚登记管理系统中打印结婚证的方法:
    void printMarriageCertificate(Person person1, Person person2) {
        System.out.println("丈夫姓名: " + personl.getName());
        System.out.println("妻子姓名: " + person2.getName());
    }
    
    这里的personlperson2没有明确区分哪个表示男性,哪个表示女性。通过修改为以下形式可以更清晰地表达意图:
    void printMarriageCertificate(Person husband, Person wife) {
        System.out.println("丈夫姓名: " + husband.getName());
        System.out.println("妻子姓名: " + wife.getName());
    }
    
  • 避免使用无意义或不具区分性的变量名如n1, n2, temp等。但字母i在循环中用作索引是一个例外,因为它代表了“index”的含义:
    for (int i = 0; i < students.size(); i++) {
        // 循环体
    }
    

1.2.3 遵循惯例

  • 命名应遵循软件开发领域及应用领域的惯例。例如,工厂类命名为StudentFactory,集合大小的方法通常使用sizelengthcount。遵循设计模式提供的命名规则,如*Factory表示工厂类,*Adapter表示适配器类。
  • 团队内部应有一致的命名规则,确保所有成员都能快速理解代码。

1.2.4 添加有意义的语境

  • 根据代码所在的上下文确定名称,必要时添加前后缀提供额外信息。例如,将state改为residenceState可以避免歧义:
    public class Student {
        private String name;
        private String residenceState; // 表示学生居住的国家
        private String residenceStreet; // 表示学生居住的街道
    }
    
    或者进一步封装为新类:
    public class ResidenceAddress {
        private String state;
        private String street;
    }
    
    public class Student {
        private String name;
        private ResidenceAddress residenceAddress;
    }
    

1.2.5 符合自然语言语法的命名

  • 单复数形式:根据对象数量选择合适的单复数形式,例如students表示多个学生。
  • 时态:一般现在时最常用,但在需要强调状态变化时可使用过去时或现在进行时。例如,获取已经选择的项目方法命名为getItemSelected(),获取正在处理中的项目则用getProcessingItems()
  • 词性选择:类名采用名词或名词短语,方法名采用动词或动词短语,属性名则为名词或形容词。类名首字母大写,因为它们定义了一个专用的概念。

1.2.6 缩略词

  • 使用缩略词可以缩短名称长度,但可能降低可读性。因此,仅对频繁使用的单词采用缩略词,并遵循英语惯例或团队内部统一的标准。例如,去掉元音MKT: market;保留前几个字母INFO: information;根据发音THRU: through
  • 一个团队应该对缩略词进行统一管理,确保在整个项目组中对同一个单词的缩略只有一种方法,并且这些缩略词被所有团队成员了解。避免滥用缩略词,特别是在单词仅出现一两次的情况下。

1.3 函数

1.3.1 单一功能

  • 定义:函数的单一功能是指一个函数只完成一件事情,实现一个功能,承担一个责任。如果一个函数只因一个原因需要修改,则其功能是单一的。
  • 判断方法:可以用一句话描述函数的功能,并且不使用“并且”(and)、“或者”(or)等连接词。例如:
    • 好的命名:calculateTotalPrice()(计算总价)
    • 不好的命名:calculateTotalPriceAndApplyDiscount()(计算总价并应用折扣)
  • 价值
    • 从函数本身的角度
      • 易于命名:单一功能的函数容易用简短、明确的名称描述。
      • 内聚性强:所有代码专注于实现单一功能。
      • 易于测试:测试用例和测试环境的搭建更简单。
    • 从使用者的角度
      • 提高开发效率:单一功能的函数易于理解,减少误用的可能性。
      • 支持团队协作:多人协作时,职责清晰,降低沟通成本。

1.3.2 抽象层次

  • 定义:函数中的所有实现语句应该在同一个抽象层次上。功能分解的过程是一个逐层细化的过程,每一层分解形成一个新的抽象层次。
  • 举例:以泡方便面为例:
    • 第一层:泡方便面(主功能)
    • 第二层:烧开水、准备方便面、注入开水
    • 第三层:烧开水 → 向水壶加水、开电源、加热水
    • 第四层:加入作料 → 加入调味料、加入蔬菜料
  • 问题:如果函数中混杂不同抽象层次的代码,读者思维会在高层次和低层次之间切换,难以把握整体逻辑或细节。
  • 解决方案:将低层次的实现封装到独立函数中,确保每个函数只处理同一抽象层次的逻辑。

1.3.3 函数长度

  • 原则:函数长度是否合适应以“是否容易理解”为最高准则。
    • 如果表达式复杂,即使两三行代码也可能过长。
    • 如果函数逻辑简单,顺序执行,超过100行也是可以接受的。
  • 封装的意义
    • 函数通过名称说明其功能,而代码只能说明实现方法。
    • 函数将“怎么做”的语义转换为“做什么”的语义,提升抽象层次。
  • 何时封装:当发现一段代码需要用注释来解释其功能时,就应该将其封装为函数,无论代码长短。

1.3.4 输入参数

  • 参数数量的影响:函数的参数越多,使用难度越大。
    • 最理想的是无参函数。
    • 其次是一元函数(一个参数)、二元函数(两个参数)。
    • 参数超过两个时,函数的使用变得困难。
  • 减少参数的方法
    1. 功能分解:如果函数需要多个参数,可能是因为功能不单一。进一步分解功能,减少参数。
      // 示例:画直线的函数
      void drawLine(int x1, int y1, int x2, int y2);
      
      可以重构为:
      void drawLine(Point startPoint, Point endPoint);
      class Point {
          private int x;
          private int y;
      }
      
    2. 从环境中获取输入:如果函数可以从所在环境中获取所需数据,则无需通过参数传入。
      public String toString() {
          return "姓名:" + this.name + ",年龄:" + this.age + ",身份证号:" + this.idNo;
      }
      
    3. 避免标识参数
      • 标识参数是指根据参数值控制函数执行不同功能的情况,破坏了函数的封装性。
      • 解决方法:将每种情况拆分为独立的函数。
        // 避免标识参数
        public void function1(int functionType) {
            if (functionType == 1) {
                // 情况1
            } else if (functionType == 2) {
                // 情况2
            }
        }
        
        重构为:
        public void handleCase1() { ... }
        public void handleCase2() { ... }
        

1.3.5 分离修改状态和查询状态的函数

  • 问题:如果一个函数同时修改状态和查询状态,会导致逻辑混乱。例如:
    public boolean addUser(User user) {
        if (this.users.contains(user)) {
            return true;
        } else {
            this.users.add(user);
            return false;
        }
    }
    
    调用时:
    if (userManager.addUser(user)) { ... }
    
    无法明确该调用是检查用户是否存在还是添加新用户。
  • 解决方案:将查询和修改分离为两个独立函数。
    public boolean userExists(User user) { ... }
    public void addUser(User user) { ... }
    

1.3.6 避免重复

  • 危害:代码重复会增加维护成本,可能导致错误修复不彻底或引入新的问题。
  • 解决方法
    1. 完全相同的代码:直接抽取为独立函数。
    2. 部分相同、部分不同的代码
      • 使用抽象提取公共部分,不同部分由子类或独立函数实现。
      • 示例:继承与多态的应用。
        // 抽取公共部分
        public abstract class BaseHandler {
            public void commonLogic() { ... }
            public abstract void specificLogic();
        }
        
        public class HandlerA extends BaseHandler {
            @Override
            public void specificLogic() { ... }
        }
        
        public class HandlerB extends BaseHandler {
            @Override
            public void specificLogic() { ... }
        }
        
    3. 关键点:判断重复的标准在于逻辑语义,而非代码长度。

1.4 类

1.4.1 封装

面向对象的封装理解:

  • 第一种理解:封装是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体。具体来说,就是将数据与操作这些数据的代码进行有机结合,形成“类”。在这样的类中,数据和函数都是类的成员。
  • 第二种理解:隐藏对象的属性和实现细节,仅对外公开接口,控制程序中对属性的读取和修改访问级别。

综合理解:

  • 面向对象的封装意味着将数据与行为结合在一起,形成一个有机整体,并且对外隐藏其实现细节,只通过公开的接口与对象通信。这种做法不仅增强了安全性,还简化了编程过程。使用者无需了解具体的实现细节,只需通过外部接口以特定的访问权限使用类的成员即可。

封装的目的:

  • 增强安全性和简化编程:通过限制直接访问内部状态,减少了错误的可能性,同时也降低了使用者的学习成本。
  • 符合人类思维习惯:将语义相近的数据和操作放在一起,更便于理解和记忆,有利于用计算模型模拟现实世界中的事物。
  • 提高可维护性:高内聚性的类由于其功能单一,通常只会因为一个理由而需要修改,这使得代码更容易维护。

隐藏实现细节的目的:

  • 降低学习成本和增加稳定性:使用者只需要知道类的功能及其使用方法,而不必深入理解复杂的实现细节,这降低了学习成本并提高了使用的稳定性。
  • 提高安全性:避免将类的私有信息暴露给外部,从而减少安全风险。例如,敏感数据或关键算法不应直接暴露给外部调用者。

类的对外接口:

  • 对外接口指的是类的公共方法,它们是对象间通信的唯一途径。为了保证对象的安全性,应将不希望外部访问的数据和实现过程设置为私有或保护级别的成员。这样可以确保只能通过公开接口来访问对象的状态,并且所有对对象的操作都必须通过发送指令的方式来进行。

1.4.2 抽象、继承、多态

1. 抽象 (Abstraction)

  • 定义:抽象是从众多事物中提取共同的、本质性的特征,而忽略非本质的细节。

    • 它关注的是“是什么”和“能做什么”,而不是“如何实现”。
    • 抽象帮助我们隐藏复杂的实现细节,提供简单的接口供外部使用。
  • 作用

    • 简化复杂性:通过提取共性特征,降低系统的复杂度。
    • 提高复用性:抽象出的通用部分可以被多个具体实现复用。
    • 增强灵活性:允许不同的具体实现根据需求进行扩展和修改。

2. 继承 (Inheritance)

  • 定义:继承是一种“分层治之”的机制,允许子类复用父类的特性,并在此基础上进行扩展或修改。

    • 继承体现了“从一般到特殊”的思想。
  • 理解角度

    • 数据抽象的角度:继承能够为现实世界进行静态建模。例如,不同类型的问题都可以看作是“问题”这一概念的具体表现形式。
    • 过程抽象的角度:继承体现了分层解决问题的思想。通用的行为由父类定义,具体的行为由子类实现。
  • 作用

    • 代码复用:子类可以直接复用父类的特性,减少重复开发。
    • 扩展性:子类可以通过修改或扩展父类的行为来满足特定需求。
    • 层次化设计:通过继承,可以将复杂问题分解为多个层次,逐层解决。

3. 多态 (Polymorphism)

  • 定义:多态是指同一个行为在不同的对象上表现出不同的结果。

    • 多态的核心在于“动态绑定”或“运行时绑定”,即程序在运行时才能决定调用哪个具体的行为。
  • 作用

    • 增强灵活性:允许程序在运行时动态适应不同的对象类型。
    • 提高可扩展性:新增类型时,无需修改现有逻辑,只需实现统一的行为规范即可。
    • 简化逻辑结构:通过多态,可以用统一的方式处理不同类型的对象。

4. 抽象、继承与多态的整体关系

  • 抽象是基础:抽象定义了通用的行为规范,为继承和多态提供了前提。
  • 继承是手段:继承通过复用和扩展实现了抽象的具体化。
  • 多态是结果:多态利用继承和抽象的特性,实现了灵活的动态行为。

5. 软件构建方法:从上到下的设计思想

  • 框架式设计

    • 先构建上层的抽象框架,定义系统的整体结构。
    • 再逐步补充具体细节,实现各个模块的功能。
    • 这种方法类似于建造房屋时先搭建框架,再填充细节。
  • 优势

    • 稳定性高:抽象框架定义了系统的骨架,减少了因需求变化导致的大规模修改。
    • 易于扩展:新增功能时,只需添加新的具体实现,无需修改现有逻辑。
    • 职责清晰:每个模块只负责自己的功能,降低了模块间的耦合。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值