码出高效:Java开发手册-第2章(6)

简介: 本章开始讲解面向对象思想,并以Java 为载体讲述面向对象思想在具体编程语言中的运用与实践。当前主流的编程语言有50 种左右,主要分为两大阵营:面向对象编程与面向过程编程。面向对象编程(Object-Oriented Programming,OOP)是划时代的编程思想变革,推动了高级语言的快速发展和工业化进程。OOP 的抽象、封装、继承、多态的理念使软件大规模化成为可能,有效地降低了软件开发成本、维护成本和复用成本。面向对象编程思想完全不同于传统的面向过程编程思想,使大型软件的开发就像搭积木一样隔离可控、高效简单,是当今编程领域的一股势不可......

2.4 方法

2.4.1 方法签名

      方法签名包括方法名称和参数列表,是JVM标识方法的唯一索引,不包括返回值,更加不包括访问权限控制符、异常类型等。假如返回值可以是方法签名的一部分,仅从代码可读性角度来考虑,如下示例:

long f() {

      return 1L;

}

double f() {

      return 1.0d;

}

var a = f();

      那么类型推断的var 到底是接收1.0d 还是1L ?从静态阅读的角度,根本无从知道它调用的是哪个方法。

2.4.2 参数

      在高中数学中计算函数f (x ,y )=x 2+2y - 3,将x =3,y =7 代入公式得到 32+2×7 -3=20,这里f (x ,y ) 的x 与y 就是形式参数,简称形参;而3 与7 是实际参数,简称实参。参数是自变量,而f (x ,y ) 函数,即代码中的方法是因变量,是一个逻辑执行的果。参数又叫parameter,在代码注释中用@param 表示参数类型。参数在方法中,属于方法签名的一部分,包括参数类型和参数个数,多个参数用逗号相隔,在代码风格中,约定每个逗号后必须要有一个空格,不管是形参,还是实参。形参是在方法定义阶段,而实参是在方法调用阶段,先来看看实参传递给形参的过程:

public class ParamPassing {

      private static int intStatic = 222;

      private static String stringStatic = "old string";

      private static StringBuilder stringBuilderStatic

             = new StringBuilder("old stringBuilder");

      public static void main(String[] args) {

             // 实参调用

             method(intStatic);

             method(stringStatic);

             method(stringBuilderStatic, stringBuilderStatic);

             // 输出依然是222 ( 第1处)

             System.out.println(intStatic);

             method();

             // 无参方法调用之后,反而修改为888 ( 第2处)

             System.out.println(intStatic);

             // 输出依然是:old string

             System.out.println(stringStatic);

             // 输出结果参考下方分析

             System.out.println(stringBuilderStatic);

      }

      // A 方法

      public static void method(int intStatic) {

             intStatic = 777;

      }

      // B 方法

      public static void method() {

             intStatic = 888;

      }

      // C 方法

      public static void method(String stringStatic) {

             // String 是immutable 对象,String 没有提供任何方法用于修改对象

             stringStatic = "new string";

      }

      // D 方法

      public static void method(StringBuilder stringBuilderStatic1,

             StringBuilder stringBuilderStatic2) {

             // 直接使用参数引用修改对象 ( 第3处)

             stringBuilderStatic1.append(".method.first-");

             stringBuilderStatic2.append("method.second-");

             // 引用重新赋值

             stringBuilderStatic1 = new StringBuilder("new stringBuilder");

             stringBuilderStatic1.append("new method's append");

      }

}

      如果不了解形参与实参的传递方式,对于第1 处和第2 处是存在疑问的。第1处,通过有参方法执行intStatic=777,居然没有修改成功,而使用无参的method 方法却成功地把静态变量intStatic 的值修改为888。字节码实现如图2-6 所示。

11.jpg

图2-6 字节码示意图


      有参的A 方法字节码如图2-6(a)所示,参数是局部变量,拷贝静态变量的777,并存入虚拟机栈中的局部变量表的第一个小格子内。虽然在方法内部的intStatic与静态变量同名,但是因为作用域就近原则,它是局部变量的参数,所有的操作与静态变量是无关的。而无参的B 方法字节码如图2-6(b)所示,先把本地赋值的888压入虚拟机栈中的操作栈,然后给静态变量intStatic 赋值。有两个参数的D 方法中,我们再分析第3 处StringBuilder 的疑问:

public static method(Ljava/lang/StringBuilder;Ljava/lang/StringBuilder;)V

   L0

   ALOAD 0

   LDC ".method.first"

   INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)

   Ljava/lang/StringBuilder;

   POP

L1

   ALOAD 1

   LDC "method.second"

   INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)

   Ljava/lang/StringBuilder;

   POP

L2

   NEW java/lang/StringBuilder

   DUP

   LDC "new stringBuilder"

   INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V

   ASTORE 0

L3

   ALOAD 0

   LDC "new method's append"

   INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)

   Ljava/lang/StringBuilder;

   POP

   RETURN

      注意上述字节码中的两个ALOAD 0,是把静态变量的引用赋值给虚拟机栈的栈帧中的局部变量表,然后ALOAD 操作是把两个对象引用变量压入操作栈的栈顶。注意,这两个引用都指向了静态引用变量指向的new StringBuilder("old stringBuilder") 对象在method(stringBuilderStatic, stringBuilderStatic) 的执行结果后的值,其中红绿字符串分别是两次append 的结果:

      old stringBuilder.method.first-method.second-

      在D 方法中,new 出来一个新的StringBuilder 对象,赋值给stringBuilderStatic1。注意,这是一个新的局部引用变量,使用ASTORE 命令对局部变量表的第一个位置的引用变量值进行了覆盖,然后再重新进行ALOAD 到操作栈顶,所以后续对于stringBuilderStatic1 的append 操作,与类的静态引用变量stringBuilderStatic 没有何关系。

      综上所述,无论是对于基本数据类型,还是引用变量,Java 中的参数传递都是值复制的传递过程。对于引用变量,复制指向对象的首地址,双方都可以通过自己的引用变量修改指向对象的相关属性。

      再来介绍一种特殊的参数——可变参数。它是在JDK5 版本中引入的,主要为了解决当时的反射机制和printf 方法问题,适用于不确定参数个数的场景。可变参数通过“参数类型...”的方式定义,如PrintStream 类中printf 方法使用了可变参数:

public PrintStream printf(String format, Object... args) {

      return format(format, args);

}

// 调用printf 方法示例

System.out.printf("%d", n); (第1处)

System.out.printf("%d %s", n, "something"); (第2处)

      如上示例代码,虽然第1 处调用传入了两个参数,第2处调用传入了三个参数,但它们调用的都是printf(String format, Object ... args) 方法。看上去可变参数使方法调用更简单,省去了手工创建数组的麻烦。有人说可变参数是语法糖,个人觉得是恶魔果实。如果在实际开发过程中使用不当,会严重影响代码的可读性和可维护性。因此,使用时要谨慎小心,尽量不要使用可变参数编程。如果一定要使用,则只有相同参数类型,相同业务含义的参数才可以,并且一个方法中只能有一个可变参数,且这个可变参数必须是该方法的最后一个参数。此外,建议不要使用Object 作为可变参数,如下警示代码:

public static void listUsers(Object... args) {

      System.out.println(args.length);

}

public static void main(String[] args) {

      // 以下代码输出结果为:3

      listUsers(1, 2, 3);

      // 以下代码输出结果为:1

      listUsers(new int[] {1, 2, 3});

      // 以下代码输出结果为:2 ( 第1处)

      listUsers(3, new String[] {"1", "2"});

      // 以下代码输出结果为:3 ( 第2处)

      listUsers(new Integer[] {1, 2, 3});

      // 以下代码输出结果为:2 ( 第3处)

      listUsers(3, new Integer[] {1, 2, 3});

}

      通过上面的例子可以看到,使用Object 作为可变参数时过于灵活,类型转换场景不好预判,比如第2 处和第3 处中Integer[] 可以转型为Object[],也可以作为一个Object 对象,所以导致第2 处输出结果为3,第3 处输出结果为2。而int[] 只能被当作一个单纯的Object 对象。同时Object 又很容易破坏“可变参数具备相同类型,相同业务含义”这个大前提,如上例中第1 处的整型和字符串数组类型混用,因此要避免使用Object 作为可变参数。

      以上是参数定义的相关内容,那么如何正确地使用参数呢?方法定义方并不能保证调用方会按照预期传入参数,因此在方法体中应该对传入的参数保持理性的不信任。方法的第一步骤并不是功能实现,而应该是参数预处理。参数预处理包括两种:

(1)入参保护。虽然“入参保护”被提及的频率和认知度远低于参数校验,但是其重要性却不能被忽略。入参保护实质上是对服务提供方的保护,常见于批量接口。批量接口是指能同时处理一批数据,但其处理能力并不是无限的,因此需要对入参的数据量进行判断和控制,如果超出处理能力,可以直接返回错误给客户端。某业务曾发生过一个严重故障,就是由一个用户批量查询的接口导致的。虽然在API 文档中约定了每次最多支持查询的用户ID 个数,但在接口实现中没有做任何入参保护,导致当调用方传入万级的用户ID 集合查询信息时,服务器内存被塞满,进程假死,再无任何处理能力。

(2)参数校验。参数作为方法间交互和传递信息的媒介,其重要性不言而喻。基于防御式编程理念,在方法内,对方法调用方传入的参数理性上保持不信任,所以对参数有效值的检测都是非常有必要的。由于方法间交互是非常频繁的,如果所有方法都进行参数校验,就会导致重复代码及不必要的检查影响代码性能。综合两个方面考虑,汇总需要进行参数校验和无须校验的场景。

      需要进行参数校验的场景:

  • 调用频度低的方法。
  • 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退或者错误,则得不偿失。
  • 需要极高稳定性和可用性的方法。
  • 对外提供的开放接口。
  • 敏感权限入口。

      不需要进行参数校验的场景:

极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查。

  • 底层调用频度较高的方法。参数错误不太可能到底层才会暴露问题。一般DAO 层与Service 层都在同一个应用中,部署在同一台服务器中,所以可以省略DAO 的参数校验。
  • 声明成 private 只会被自己代码调用的方法。如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。
相关文章
|
4月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
7月前
|
人工智能 前端开发 Java
2025年WebStorm高效Java开发全指南:从配置到实战
WebStorm 2025不仅是一款强大的JavaScript IDE,也全面支持Java开发。本文详解其AI辅助编程、Java特性增强及性能优化,并提供环境配置、高效开发技巧与实战案例,助你打造流畅的全栈开发体验。
645 4
|
7月前
|
前端开发 JavaScript Java
Java 开发中 Swing 界面嵌入浏览器实现方法详解
摘要:Java中嵌入浏览器可通过多种技术实现:1) JCEF框架利用Chromium内核,适合复杂网页;2) JEditorPane组件支持简单HTML显示,但功能有限;3) DJNativeSwing-SWT可内嵌浏览器,需特定内核支持;4) JavaFX WebView结合Swing可完美支持现代网页技术。每种方案各有特点,开发者需根据项目需求选择合适方法,如JCEF适合高性能要求,JEditorPane适合简单展示。(149字)
909 1
|
7月前
|
安全 Java 领域建模
Java 17 探秘:不容错过的现代开发利器
Java 17 探秘:不容错过的现代开发利器
487 0
|
5月前
|
消息中间件 人工智能 Java
抖音微信爆款小游戏大全:免费休闲/竞技/益智/PHP+Java全筏开源开发
本文基于2025年最新行业数据,深入解析抖音/微信爆款小游戏的开发逻辑,重点讲解PHP+Java双引擎架构实战,涵盖技术选型、架构设计、性能优化与开源生态,提供完整开源工具链,助力开发者从理论到落地打造高留存、高并发的小游戏产品。
|
5月前
|
存储 Java 关系型数据库
Java 项目实战基于面向对象思想的汽车租赁系统开发实例 汽车租赁系统 Java 面向对象项目实战
本文介绍基于Java面向对象编程的汽车租赁系统技术方案与应用实例,涵盖系统功能需求分析、类设计、数据库设计及具体代码实现,帮助开发者掌握Java在实际项目中的应用。
239 0
|
6月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
341 3
|
6月前
|
安全 Java 数据库
Java 项目实战病人挂号系统网站设计开发步骤及核心功能实现指南
本文介绍了基于Java的病人挂号系统网站的技术方案与应用实例,涵盖SSM与Spring Boot框架选型、数据库设计、功能模块划分及安全机制实现。系统支持患者在线注册、登录、挂号与预约,管理员可进行医院信息与排班管理。通过实际案例展示系统开发流程与核心代码实现,为Java Web医疗项目开发提供参考。
349 2