标签 Java 下的文章 - 酷游博客
首页
关于
友链
Search
1
阿里的简历多久可以投递一次?次数多了有没有影响?可以同时进行吗?
45 阅读
2
Java中泛型的理解
40 阅读
3
Java 14 发布了,再也不怕 NullPointerException 了!
38 阅读
4
Java中的可变参数
37 阅读
5
该如何创建字符串,使用" "还是构造函数?
29 阅读
技术
登录
/
注册
找到
191
篇与
Java
相关的结果
2025-01-22
Java 14 发布了,再也不怕 NullPointerException 了!
2020年3月17日发布,Java正式发布了JDK 14 ,目前已经可以开放下载。在JDK 14中,共有16个新特性,本文主要来介绍其中的一个特性:JEP 358: Helpful NullPointerExceptions null何错之有? 对于Java程序员来说,null是令人头痛的东西。时常会受到空指针异常(NullPointerException)的骚扰。相信很多程序员都特别害怕出现程序中出现NPE,因为这种异常往往伴随着代码的非预期运行。 在编程语言中,空引用(Null Reference)是一个与空指针类似的概念,是一个已宣告但其并未引用到一个有效对象的变量。 在Java 1 中就包含了了Null引用和NPE了,但是其实,Null引用是伟大的计算机科学家Tony Hoare 早在1965年发明的,最初作为编程语言ALGOL W的一部分。 1965年,英国一位名为Tony Hoare的计算机科学家在设计ALGOL W语言时提出了null引用的想法。ALGOL W是第一批在堆上分配记录的类型语言之一。Hoare选择null引用这种方式,“只是因为这种方法实现起来非常容易”。虽然他的设计初衷就是要“通过编译器的自动检测机制,确保所有使用引用的地方都是绝对安全的”,他还是决定为null引用开个绿灯,因为他认为这是为“不存在的值”建模最容易的方式。 但是在2009年,很多年后,他开始为自己曾经做过这样的决定而后悔不已,把它称为“一个价值十亿美元的错误”。实际上,Hoare的这段话低估了过去五十年来数百万程序员为修复空引用所耗费的代价。因为在ALGOL W之后出现的大多数现代程序设计语言,包括Java,都采用了同样的设计方式,其原因是为了与更老的语言保持兼容,或者就像Hoare曾经陈述的那样,“仅仅是因为这样实现起来更加容易”。  相信很多Java程序员都一样对null和NPE深恶痛绝,因为他确实会带来各种各样的问题(来自《Java 8 实战》)。如: 它是错误之源。 NullPointerException是目前Java程序开发中最典型的异常。它会使你的代码膨胀。 它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。 它自身是毫无意义的。 null自身没有任何的语义,尤其是是它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。 它破坏了Java的哲学。 Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。 它在Java的类型系统上开了个口子。 null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题, 原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初赋值到底是什么类型。 其他语言如何解决NPE问题 我们知道,出了Java语言外,还有很多其他的面向对象语言,那么在其他的一些语言中,是如何解决NPE的问题的呢? 如在Groovy中使用安全导航操作符(Safe Navigation Operator)可以访问可能为null的变量: def carInsuranceName = person?.car?.insurance?.name Groovy的安全导航操作符能够避免在访问这些可能为null引用的变量时发生NullPointerException,在调用链中的变量遭遇null时将null引用沿着调用链传递下去,返回一个null。 其实这个功能曾经考虑过增加一个类似的功能,但是后来又被舍弃了。 另外,在Haskell和Scala也有类似的替代品,如Haskell中的Maybe类型、Scala中的Option[T]。 在 Kotlin 中,其类型系统严格区分一个引用可以容纳 null 还是不能容纳。也就是说,一个变量是否可空必须显示声明,对于可空变量,在访问其成员时必须做空处理,否则无法编译通过: var a: String = "abc" a = null // 编译错误 果允许为空,可以声明一个可空字符串,写作 String?: var b: String? = "abc" //String? 表示该 String 类型变量可为空 b = null // 编译通过 看到这个?的时候,是不是发现和Groovy有点像?不过还是有一定区别的,这里就不展开了。 好了,书归正传,我们来看看作为一个TOIBE编程语言排行榜第一名的语言,Java语言对于NPE做出了哪些努力! Java做了哪些努力 一直以来对于null和NPE的改进还是做出了一些努力的。 首先在Java 8中提供了Optional,其实在Java 8 推出之前,Google的Guava库中就率先提供过Optional接口来使null快速失败。 Optional在可能为null的对象上做了一层封装,Optional对象包含了一些方法来显式地处理某个值是存在还是缺失,Optional类强制你思考值不存在的情况,这样就能避免潜在的空指针异常。 但是设计Optional类的目的并不是完全取代null,它的目的是设计更易理解的API。通过Optional,可以从方法签名就知道这个函数有可能返回一个缺失的值,这样强制你处理这些缺失值的情况。 关于Optional的用法,不是本文的重点,就不在这里详细介绍了,笔者在日常开发中经常结合Stream一起使用Optional,还是比较好用的。 另外一个值得一提的就是最近(2020年03月17日)发布的JDK 14中对于NPE有了一个增强。那就是JEP 358: Helpful NullPointerExceptions 更有帮助的NPE JDK 14中对于NEP有了一个增强,既然NPE暂时无法避免,那么就让他对开发者更有帮助一些。  每个Java开发人员都遇到过NullPointerExceptions (NPEs)。由于NPEs可以发生在程序的几乎任何地方,试图捕获并从它们中恢复通常是不切实际的。因此,开发人员通常依赖于JVM来确定NPE实际发生时的来源。例如,假设在这段代码中出现了一个NPE: a.i = 99; JVM将打印出导致NPE的方法、文件名和行号: Exception in thread "main" java.lang.NullPointerException at Prog.main(Prog.java:5) 通过以上堆栈信息,开发人员可以定位到a.i= 99这一行,并推断出a一定是null。 但是,对于更复杂的代码,如果不使用调试器,就不可能确定哪个变量是null。假设在这段代码中出现了一个NPE: a.b.c.i = 99; 我们根本无法确定到底是a还是b或者是c在运行时是个null值。 但是,在JDK14以后,这种窘境就有解了。 在JDK14中,当运行期,试图对一个null对象进行应用时,JVM依然会抛出一个NullPointerException (NPE),除此之外,还会通过通过分析程序的字节码指令,JVM将精确地确定哪个变量是null,并且在堆栈信息中明确的提示出来。 在JDK 14中,如果上文中的a.i = 99发生NPE,将会打印如下堆栈: Exception in thread "main" java.lang.NullPointerException: Cannot assign field "i" because "a" is null at Prog.main(Prog.java:5) 如果是a.b.c.i = 99;中的b为null导致了空指针,则会打印以下堆栈信息: Exception in thread "main" java.lang.NullPointerException: Cannot read field "c" because "a.b" is null at Prog.main(Prog.java:5) 可见,堆栈中明确指出了到底是哪个对象为null而导致了NPE,这样,一旦应用中发生NPE,开发者可以通过堆栈信息第一时间定位到到底是代码中的那个对象为null导致的。 这算是JDK的一个小小的改进,但是这个改进对于开发者来说确实是非常友好的。真的希望这些小而美的改动可以在JDK中越来越多。 参考资料: https://openjdk.java.net/jeps/358 《Java 8 In Action》
技术
# Java
酷游
1月22日
0
38
0
2025-01-22
Java中的可变参数
什么是可变参数 可变参数(variable arguments)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。 public static void main(String[] args) { print("a"); print("a", "b"); print("a", "b", "c"); } public static void print(String ... s){ for(String a: s) System.out.println(a); } 可变参数的工作原理 可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。 可变参数的使用场景 通过其定义我们知道,当一个方法可能要接收任意数量的参数的时候使用可变参数是非常有用的。Java SDK中有个很好的例子:String.format(String format, Object... args)。一个字符串可以中可以包含任意个待格式化参数,所以使用可变参数。 String.format("An integer: %d", i); String.format("An integer: %d and a string: %s", i, s);
技术
# Java
酷游
1月22日
0
37
0
2025-01-22
Java中泛型的理解
Java泛型(generics) 是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。对于泛型概念的引入,开发社区的观点是褒贬不一。从好的方面来说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误。而从不好的地方来说,为了保证与旧有版本的兼容性,Java泛型的实现上存在着一些不够优雅的地方。当然这也是任何有历史的编程语言所需要承担的历史包袱。后续的版本更新会为早期的设计缺陷所累。开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试将一个List的对象作为实际参数传进去,却发现无法通过编译。虽然从直觉上来说,Object是String的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。本文试图对Java泛型做一个概括性的说明。 类型擦除 Java的类型擦除 正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List 和List等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。 很多泛型的奇怪特性都与这个类型擦除的存在有关,包括: 泛型类并没有自己独有的Class类对象。比如并不存在List.class或是List.class,而只有List.class。 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass;还是new MyClass创建的对象,都是共享一个静态变量。 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException;和MyException的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。 类型擦除的基本过程也比较简单: 首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉的内容。比如: T get()方法声明就变成了Object get(); List就变成了List。 接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码: class MyString implements Comparable { public int compareTo(String str) { return 0; } } 当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。 实例分析 了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List和List为例来具体分析: public void inspect(List list) { for (Object obj : list) { System.out.println(obj); } list.add(1); //这个操作在当前方法的上下文是合法的。 } public void test() { List strs = new ArrayList(); inspect(strs); //编译错误 } 这段代码中,inspect方法接受List作为参数,当在test方法中试图传入List的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。 通配符与上下界 (可参考 Java泛型中extends和super的理解 和 Java泛型中K T V E ? object等的含义) 在使用泛型类的时候,既可以指定一个具体的类型,如List就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List所声明的就是所有类型都是可以的。但是List并不等同于List。List实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用。而List则其中所包含的元素类型是不确定。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。考虑下面的代码: public void wildcard(List list) { list.add(1);//编译错误 } 如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。 因为对于List中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如List即可。List是List的子类型,因此传递参数时不会发生错误。 开发自己的泛型类 泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用声明的类型参数。一个类可以有多个类型参数,如 MyClass。 每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。 class ClassTest { private X x; private static Y y; //编译错误,不能用在静态变量中 public X getFirst() { //正确用法 return x; } public void wrong() { Z z = new Z(); //编译错误,不能创建对象 } } 总结 在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。 在代码中避免泛型类和原始类型的混用(Effective Java中建议不要在代码中使用原始类型)。比如List 和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。 泛型类最好不要同数组一块使用。你只能创建new List[10]这样的数组,无法创建new List [10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。 不要忽视编译器给出的警告信息。 参考资料: Java深度历险(五)——Java泛型
技术
# Java
酷游
1月22日
0
40
0
2025-01-22
我要彻底给你讲清楚,Java就是值传递,不接受争辩的那种!
关于Java中方法间的参数传递到底是怎样的、为什么很多人说Java只有值传递等问题,一直困惑着很多人,甚至我在面试的时候问过很多有丰富经验的开发者,他们也很难解释的很清楚。 我很久也写过一篇文章,我当时认为我把这件事说清楚了,但是,最近在整理这部分知识点的时候,我发现我当时理解的还不够透彻,于是我想着通过Google看看其他人怎么理解的,但是遗憾的是没有找到很好的资料可以说的很清楚。 于是,我决定尝试着把这个话题总结一下,重新理解一下这个问题。 辟谣时间 关于这个问题,在StackOverflow上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,但是说不出来为什么。 在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了。如果你有以下想法,那么你有必要好好阅读本文。 错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。 错误理解二:Java是引用传递。 错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。 实参与形参 我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法,public static void main(String[] args),这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。 实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。 简单举个例子: public static void main(String[] args) { ParamTest pt = new ParamTest(); pt.sout("Hollis");//实际参数为 Hollis } public void sout(String name) { //形式参数为 name System.out.println(name); } 实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。 求值策略 我们说当进行方法调用的时候,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢? 这其实是程序设计中求值策略(Evaluation strategies)的概念。 在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。 求值策略分为两大基本类,基于如何处理给函数的实际参数,分位严格的和非严格的。 严格求值 在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。多数现存编程语言对函数都使用严格求值。所以,我们本文只关注严格求值。 在严格求值中有几个关键的求值策略是我们比较关心的,那就是传值调用(Call by value)、传引用调用(Call by reference)以及传共享对象调用(Call by sharing)。 传值调用(值传递) 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个”局部拷贝”,所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。 传引用调用(应用传递) 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。 传共享对象调用(共享对象传递) 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为”传共享对象”,所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。 不知道大家有没有发现,其实传共享对象调用和传值调用的过程几乎是一样的,都是进行”求值”、”拷贝”、”传递”。你品,你细品。  但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响。你再品,你再细品。 那么,共享对象传递和值传递以及引用传递之间到底有很么关系呢? 对于这个问题,我们应该关注过程,而不是结果,因为传共享对象调用的过程和传值调用的过程是一样的,而且都有一步关键的操作,那就是”复制”,所以,通常我们认为传共享对象调用是传值调用的特例 我们先把传共享对象调用放在一边,我们再来回顾下传值调用和传引用调用的主要区别: 传值调用是指在调用函数时将实际参数复制一份传递到函数中,传引用调用是指在调用函数时将实际参数的引用直接传递到函数中。 所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。 这里我们来举一个形象的例子。再来深入理解一下传值调用和传引用调用: 你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。 这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。 你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。 这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。 Java的求值策略 前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java中是采用的哪种求值策略呢? 很多人说Java中的基本数据类型是值传递的,这个基本没有什么可以讨论的,普遍都是这样认为的。 但是,有很多人却误认为Java中的对象传递是引用传递。之所以会有这个误区,主要是因为Java中的变量和对象之间是有引用关系的。Java语言中是通过对象的引用来操纵对象的。所以,很多人会认为对象的传递是引用的传递。 而且很多人还可以举出以下的代码示例: public static void main(String[] args) { Test pt = new Test(); User hollis = new User(); hollis.setName("Hollis"); hollis.setGender("Male"); pt.pass(hollis); System.out.println("print in main , user is " + hollis); } public void pass(User user) { user.setName("hollischuang"); System.out.println("print in pass , user is " + user); } 输出结果: print in pass , user is User{name='hollischuang', gender='Male'} print in main , user is User{name='hollischuang', gender='Male'} 可以看到,对象类型在被传递到pass方法后,在方法内改变了其内容,最终调用方main方法中的对象也变了。 所以,很多人说,这和引用传递的现象是一样的,就是在方法内改变参数的值,会影响到调用方。 但是,其实这是走进了一个误区。 Java中的对象传递 很多人通过代码示例的现象说明Java对象是引用传递,那么我们就从现象入手,先来反驳下这个观点。 我们前面说过,无论是值传递,还是引用传递,只不过是求值策略的一种,那求值策略还有很多,比如前面提到的共享对象传递的现象和引用传递也是一样的。那凭什么就说Java中的参数传递就一定是引用传递而不是共享对象传递呢? 那么,Java中的对象传递,到底是哪种形式呢?其实,还真的就是共享对象传递。 其实在 《The Java™ Tutorials》中,是有关于这部分内容的说明的。首先是关于基本类型描述如下: Primitive arguments, such as an int or a double, are passed into methods by value. This means that any changes to the values of the parameters exist only within the scope of the method. When the method returns, the parameters are gone and any changes to them are lost. 即,原始参数通过值传递给方法。这意味着对参数值的任何更改都只存在于方法的范围内。当方法返回时,参数将消失,对它们的任何更改都将丢失。 关于对象传递的描述如下: Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level. 也就是说,引用数据类型参数(如对象)也按值传递给方法。这意味着,当方法返回时,传入的引用仍然引用与以前相同的对象。但是,如果对象字段具有适当的访问级别,则可以在方法中更改这些字段的值。 这一点官方文档已经很明确的指出了,Java就是值传递,只不过是把对象的引用当做值传递给方法。你细品,这不就是共享对象传递么? 其实Java中使用的求值策略就是传共享对象调用,也就是说,Java会将对象的地址的拷贝传递给被调函数的形式参数。只不过”传共享对象调用”这个词并不常用,所以Java社区的人通常说”Java是传值调用”,这么说也没错,因为传共享对象调用其实是传值调用的一个特例。 值传递和共享对象传递的现象冲突吗? 看到这里很多人可能会有一个疑问,既然共享对象传递是值传递的一个特例,那么为什么他们的现象是完全不同的呢? 难道值传递过程中,如果在被调方法中改变了值,也有可能会对调用者有影响吗?那到底什么时候会影响什么时候不会影响呢? 其实是不冲突的,之所以会有这种疑惑,是因为大家对于到底是什么是”改变值”有误解。 我们先回到上面的例子中来,看一下调用过程中实际上发生了什么? 在参数传递的过程中,实际参数的地址0X1213456被拷贝给了形参。这个过程其实就是值传递,只不过传递的值得内容是对象的应用。 那为什么我们改了user中的属性的值,却对原来的user产生了影响呢? 其实,这个过程就好像是:你复制了一把你家里的钥匙给到你的朋友,他拿到钥匙以后,并没有在这把钥匙上做任何改动,而是通过钥匙打开了你家里的房门,进到屋里,把你家的电视给砸了。 这个过程,对你手里的钥匙来说,是没有影响的,但是你的钥匙对应的房子里面的内容却是被人改动了。 也就是说,Java对象的传递,是通过复制的方式把引用关系传递了,如果我们没有改引用关系,而是找到引用的地址,把里面的内容改了,是会对调用方有影响的,因为大家指向的是同一个共享对象。 那么,如果我们改动一下pass方法的内容: public void pass(User user) { user = new User(); user.setName("hollischuang"); System.out.println("print in pass , user is " + user); } 上面的代码中,我们在pass方法中,重新new了一个user对象,并改变了他的值,输出结果如下: print in pass , user is User{name='hollischuang', gender='Male'} print in main , user is User{name='Hollis', gender='Male'} 再看一下整个过程中发生了什么: 这个过程,就好像你复制了一把钥匙给到你的朋友,你的朋友拿到你给他的钥匙之后,找个锁匠把他修改了一下,他手里的那把钥匙变成了开他家锁的钥匙。这时候,他打开自己家,就算是把房子点了,对你手里的钥匙,和你家的房子来说都是没有任何影响的。 所以,Java中的对象传递,如果是修改引用,是不会对原来的对象有任何影响的,但是如果直接修改共享对象的属性的值,是会对原来的对象有影响的。 总结 我们知道,编程语言中需要进行方法间的参数传递,这个传递的策略叫做求值策略。 在程序设计中,求值策略有很多种,比较常见的就是值传递和引用传递。还有一种值传递的特例——共享对象传递。 值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。 在Java中,其实是通过值传递实现的参数传递,只不过对于Java对象的传递,传递的内容是对象的引用。 我们可以总结说,Java中的求值策略是共享对象传递,这是完全正确的。 但是,为了让大家都能理解你说的,我们说Java中只有值传递,只不过传递的内容是对象的引用。这也是没毛病的。 但是,绝对不能认为Java中有引用传递。 OK,以上就是本文的全部内容,不知道本文是否帮助你解开了你心中一直以来的疑惑。欢迎留言说一下你的想法。 参考资料 The Java™ Tutorials Evaluation strategy Is Java “pass-by-reference” or “pass-by-value”? Passing by Value vs. by Reference Visual Explanation
技术
# Java
酷游
1月22日
0
8
0
2025-01-22
Java 8中字符串拼接新姿势:StringJoiner
在为什么阿里巴巴不建议在for循环中使用”+”进行字符串拼接一文中,我们介绍了几种Java中字符串拼接的方式,以及优缺点。其中还有一个重要的拼接方式我没有介绍,那就是Java 8中提供的StringJoiner ,本文就来介绍一下这个字符串拼接的新兵。 如果你想知道一共有多少种方法可以进行字符串拼接,教你一个简单的办法,在Intellij IDEA中,定义一个Java Bean,然后尝试使用快捷键自动生成一个toString方法,IDEA会提示多种toString生成策略可供选择。  目前我使用的IDEA的toString生成策略默认的是使用JDK 1.8提供的StringJoiner。 介绍 StringJoiner是java.util包中的一个类,用于构造一个由分隔符分隔的字符序列(可选),并且可以从提供的前缀开始并以提供的后缀结尾。虽然这也可以在StringBuilder类的帮助下在每个字符串之后附加分隔符,但StringJoiner提供了简单的方法来实现,而无需编写大量代码。 StringJoiner类共有2个构造函数,5个公有方法。其中最常用的方法就是add方法和toString方法,类似于StringBuilder中的append方法和toString方法。 用法 StringJoiner的用法比较简单,下面的代码中,我们使用StringJoiner进行了字符串拼接。 public class StringJoinerTest { public static void main(String[] args) { StringJoiner sj = new StringJoiner("Hollis"); sj.add("hollischuang"); sj.add("Java干货"); System.out.println(sj.toString()); StringJoiner sj1 = new StringJoiner(":","[","]"); sj1.add("Hollis").add("hollischuang").add("Java干货"); System.out.println(sj1.toString()); } } 以上代码输出结果: hollischuangHollisJava干货 [Hollis:hollischuang:Java干货] 值得注意的是,当我们StringJoiner(CharSequence delimiter)初始化一个StringJoiner的时候,这个delimiter其实是分隔符,并不是可变字符串的初始值。 StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix)的第二个和第三个参数分别是拼接后的字符串的前缀和后缀。 原理 介绍了简单的用法之后,我们再来看看这个StringJoiner的原理,看看他到底是如何实现的。主要看一下add方法: public StringJoiner add(CharSequence newElement) { prepareBuilder().append(newElement); return this; } private StringBuilder prepareBuilder() { if (value != null) { value.append(delimiter); } else { value = new StringBuilder().append(prefix); } return value; } 看到了一个熟悉的身影——StringBuilder ,没错,StringJoiner其实就是依赖StringBuilder实现的,在为什么阿里巴巴不建议在for循环中使用”+”进行字符串拼接中我们介绍过StringBuilder的实现原理,本文不在赘述。 当我们发现StringJoiner其实是通过StringBuilder实现之后,我们大概就可以猜到,他的性能损耗应该和直接使用StringBuilder差不多! 为什么需要StringJoiner 在了解了StringJoiner的用法和原理后,可能很多读者就会产生一个疑问,明明已经有一个StringBuilder了,为什么Java 8中还要定义一个StringJoiner呢?到底有什么好处呢? 如果读者足够了解Java 8的话,或许可以猜出个大概,这肯定和Stream有关。 作者也在Java doc中找到了答案: A StringJoiner may be employed to create formatted output from a Stream using Collectors.joining(CharSequence) 试想,在Java中,如果我们有这样一个List: List list = ImmutableList.of("Hollis","hollischuang","Java干货"); 如果我们想要把他拼接成一个以下形式的字符串: Hollis,hollischuang,Java干货 可以通过以下方式: StringBuilder builder = new StringBuilder(); if (!list.isEmpty()) { builder.append(list.get(0)); for (int i = 1, n = list.size(); i < n; i++) { builder.append(",").append(list.get(i)); } } builder.toString(); 还可以使用: list.stream().reduce(new StringBuilder(), (sb, s) -> sb.append(s).append(','), StringBuilder::append).toString(); 但是输出结果稍有些不同,需要进行二次处理: Hollis,hollischuang,Java干货, 还可以使用”+”进行拼接: list.stream().reduce((a,b)->a + "," + b).toString(); 以上几种方式,要么是代码复杂,要么是性能不高,或者无法直接得到想要的结果。 为了满足类似这样的需求,Java 8中提供的StringJoiner就派上用场了。以上需求只需要一行代码: list.stream().collect(Collectors.joining(":")) 即可。上面用的表达式中,Collectors.joining的源代码如下: public static Collector joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { return new CollectorImpl( () -> new StringJoiner(delimiter, prefix, suffix), StringJoiner::add, StringJoiner::merge, StringJoiner::toString, CH_NOID); } 其实现原理就是借助了StringJoiner。 当然,或许在Collector中直接使用StringBuilder似乎也可以实现类似的功能,只不过稍微麻烦一些。所以,Java 8中提供了StringJoiner来丰富Stream的用法。 而且StringJoiner也可以方便的增加前缀和后缀,比如我们希望得到的字符串是[Hollis,hollischuang,Java干货]而不是Hollis,hollischuang,Java干货的话,StringJoiner的优势就更加明显了。 总结 本文介绍了Java 8中提供的可变字符串类——StringJoiner,可以用于字符串拼接。 StringJoiner其实是通过StringBuilder实现的,所以他的性能和StringBuilder差不多,他也是非线程安全的。 如果日常开发中中,需要进行字符串拼接,如何选择? 1、如果只是简单的字符串拼接,考虑直接使用”+”即可。 2、如果是在for循环中进行字符串拼接,考虑使用StringBuilder和StringBuffer。 3、如果是通过一个List进行字符串拼接,则考虑使用StringJoiner。
技术
# Java
酷游
1月22日
0
8
0
1
2
...
39
下一页
易航博客