开发者社区> 博文视点Broadview> 正文

码出高效: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 只会被自己代码调用的方法。如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
高级开发必须理解的Java中SPI机制
高级开发必须理解的Java中SPI机制
25 0
六年Java开发,分享年薪50W+架构师一路成长的辛酸
13年下半年接触java,奇遇一般参加了java培训,期间甘苦自知。14年初如愿找到人生第一份工作,工资3k;对于之前的付出也算是回报吧 ,对于当时的我已经很满足了。 但是后来没想到公司是个坑, 入司半年有余,写的代码屈指可数;但是却结交了一位良师益友对我以后的职业发展和技术上提供了明灯,给予了不少帮助。所以说(塞翁失马,焉知非福)还是有些道理的。
29 0
阿里大牛都在读的10本Java实战书籍,Java开发进阶必备书单
关乎于程序员,除了做项目来提高自身的技术,还有一种提升自己的专业技能就是:多!看!书! 毕竟,书是学习的海洋呢!So,Java程序员你们准备好了吗?双手奉上Java程序员必读之热门书单。
36 0
3分钟教你用java开发一个小程序后台服务器~看完你也会
3分钟教你用java开发一个小程序后台服务器~看完你也会
32 0
一日一技:在Python开发中,如何让Java程序员抓狂
一日一技:在Python开发中,如何让Java程序员抓狂
14 0
【一名资深Java开发的经验浅谈】
【一名资深Java开发的经验浅谈】
31 0
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏21之enemy行走和死亡动画效果
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏21之enemy行走和死亡动画效果
52 0
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏20之enemy被攻击显示后退动画(block效果)
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏20之enemy被攻击显示后退动画(block效果)
39 0
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏19敌人可以被打死
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏19敌人可以被打死
32 0
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏18玩家攻击动画实现
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏18玩家攻击动画实现
37 0
+关注
博文视点Broadview
博文视点( Broadview )是电子工业出版社下属旗舰级子公司。在IT出版领域打磨多年,以敏锐眼光、独特视角密切关注技术发展趋势及变化,致力于将技术大师之优秀思想、一线专家之一流经验集结成书,为众多朋友奉献经典著作,助力个人、团队成长。
文章
问答
视频
文章排行榜
最热
最新
相关课程
更多
相关电子书
更多
JAVA开发手册1.5.0
立即下载
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
相关实验场景
更多