码出高效: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 只会被自己代码调用的方法。如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。
相关文章
|
1天前
|
SQL JavaScript 前端开发
用Java来开发Hive应用
用Java来开发Hive应用
14 7
|
1天前
|
SQL JavaScript 前端开发
用Java、Python来开发Hive应用
用Java、Python来开发Hive应用
11 6
消息中间件 缓存 监控
12 0
|
1天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
10天前
|
数据采集 Java 数据挖掘
Java IO异常处理:在Web爬虫开发中的实践
Java IO异常处理:在Web爬虫开发中的实践
|
12天前
|
数据采集 存储 前端开发
Java爬虫开发:Jsoup库在图片URL提取中的实战应用
Java爬虫开发:Jsoup库在图片URL提取中的实战应用
|
16天前
|
存储 搜索推荐 Java
探索安卓开发中的自定义视图:打造个性化UI组件Java中的异常处理:从基础到高级
【8月更文挑战第29天】在安卓应用的海洋中,一个独特的用户界面(UI)能让应用脱颖而出。自定义视图是实现这一目标的强大工具。本文将通过一个简单的自定义计数器视图示例,展示如何从零开始创建一个具有独特风格和功能的安卓UI组件,并讨论在此过程中涉及的设计原则、性能优化和兼容性问题。准备好让你的应用与众不同了吗?让我们开始吧!
|
17天前
|
小程序 JavaScript Java
【资料】阿里Java开发手册
本文是关于分享阿里Java开发手册资源及促进编程规范学习的指南。作者以个人经历引入,讲述了公司领导通过细致讲解阿里Java开发手册,提升了团队对代码质量和编程规范的认识
239 0
【资料】阿里Java开发手册
|
23天前
|
IDE Java 开发工具
快速上手指南:如何用Spring Boot开启你的Java开发之旅?
【8月更文挑战第22天】Spring Boot由Pivotal团队开发,简化了Spring应用的创建过程。本文详述了从零开始搭建Spring Boot项目的步骤:首先确保安装了新版JDK、Maven/Gradle及IDE如IntelliJ IDEA或Eclipse;接着访问Spring Initializr网站(start.spring.io),选择所需依赖(如Web模块)并生成项目;最后,使用IDE打开生成的项目,添加`@SpringBootApplication`注解及main方法来启动应用。通过这些步骤,即便是新手也能快速上手,专注于业务逻辑的实现。
31 1
|
25天前
|
Java 持续交付 项目管理
Maven是一款基于Apache许可的项目管理和构建自动化工具,在Java开发中极为流行。
Maven是一款基于Apache许可的项目管理和构建自动化工具,在Java开发中极为流行。它采用项目对象模型(POM)来描述项目,简化构建流程。Maven提供依赖管理、标准构建生命周期、插件扩展等功能,支持多模块项目及版本控制。在Java Web开发中,Maven能够自动生成项目结构、管理依赖、自动化构建流程并运行多种插件任务,如代码质量检查和单元测试。遵循Maven的最佳实践,结合持续集成工具,可以显著提升开发效率和项目质量。
36 1