精华内容
下载资源
问答
  • Java没有值类型

    2016-11-13 16:54:33
    Java值类型吗? 2016/08/31 | 分类: 基础技术 | 1 条评论 | 标签: 基础技术 分享到:3 原文出处: 正义的花生 有人看了我之前的文章『Swift 语言的设计错误』,问我:“你说 Java ...

    Java有值类型吗?

    有人看了我之前的文章『Swift 语言的设计错误』,问我:“你说 Java 只有引用类型(reference type),但是根据 Java 的官方文档,Java 也有值类型(value type)和引用类型的区别的。比如 int,boolean 等原始类型就是值类型。” 现在我来解释一下这个问题。

    Java 有值类型,原始类型 int,boolean 等是值类型,其实是长久以来的一种误解,它混淆了实现和语义的区别。不要以为 Java 的官方文档那样写就是权威定论,就可以说“王垠不懂” :) 当你认为王垠不懂一个初级问题的时候,都需要三思,因为他可能是大智若愚…… 看了我下面的论述,也许你会发现自己应该怀疑的是,Java 的设计者到底有没有搞明白这个问题 :P

    胡扯结束,现在来说正事。Java,Scheme 等语言的原始类型,比如 char,int,boolean,double 等,在“实现”上确实是通过值(而不是引用,或者叫指针)直接传递的,然而这完全是一种为了效率的优化(叫做 inlining)。这种优化对于程序员应该是不可见的。Java 继承了 Scheme/Lisp 的衣钵,它们在“语义”上其实是没有值类型的。

    这不是天方夜谭,为了理解这一点,你可以做一个很有意思的思维实验。现在你把 Java 里面所有的原始类型都“想象”成引用类型,也就是说,所有的 int, boolean 等原始类型的变量都不包含实际的数据,而是引用(或者叫指针),指向堆上分配的数据。然后你会发现这样“改造后”的 Java,仍然符合现有 Java 代码里能看到的一切现象。也就是说,原始类型被作为值类型还是引用类型,对于程序员完全没有区别。

    举个简单的例子,如果我们把 int 的实现变成完全的引用,然后来看这段代码:

    1
    2
    3
    int x = 1;    // x指向内存地址A,内容是整数1
    int y = x;    // y指向同样的内存地址A,内容是整数1
    x = 2;        // x指向另一个内存地址B,内容是整数2。y仍然指向地址A,内容是1。

    由于我们改造后的 Java 里面 int 变量全部是引用,所以第一行定义的 x 并不包含一个整数,而是一个引用,它指向堆里分配的一块内存,这个空间的内容是整数 1。在第二行,我们定 int 变量 y,当然它也是一个引用,它的值跟 x 一样,所以 y 也指向同一个地址,里面的内容是同一个整数:1。在第三行,我们对 x 这个引用赋值。你会发现一个很有意思的现象,虽然 x 指向了 2,y 却仍然指向 1。对 x 赋值并没能改变 y 指向的内容,这种情况就跟 int 是值类型的时候一模一样!所以现在虽然 int 变量全部是引用,你却不能实现共享地址的引用能做的事情:对 x 进行某种操作,导致 y 指向的内容也发生改变。

    出现这个现象的原因是,虽然现在 int 成了引用类型,你却并不能对它进行引用类型所特有(而值类型没有)的操作。这样的操作包括:

    1. deref。就像 C 语言里的 * 操作符。
    2. 成员赋值。就像对 C struct 成员的 x.foo = 2 。

    在 Java 里,你没法写像 C 语言的 *x = 2 这样的代码,因为 Java 没有提供 deref 操作符 *。你也没法通过 x.foo = 2 这样的语句改变 x 所指向的内存数据(内容是1)的一部分,因为 int 是一个原始类型。最后你发现,你只能写 x = 2,也就是改变 x 这个引用本身的指向。x = 2 执行之后,原来数字 1 所在的内存空间并没有变成 2,只不过 x 指向了新的地址,那里装着数字 2 而已。指向 1 的其它引用变量比如 y,不会因为你进行了 x = 2 这个操作而看到 2,它们仍然看到原来那个1……

    在这种 int 是引用的 Java 里,你对 int 变量 x 能做的事情只有两种:

    1. 读出它的值。
    2. 对它进行赋值,使它指向另一个地方。

    这两种事情,就跟你能对值类型能做的两件事情没有区别。这就是为什么你没法通过对 x 的操作而改变 y 表示的值。所以不管 int 在实现上是传递值还是传递引用,它们在语义上都是等价的。也就是说,原始类型是值类型还是引用类型,对于程序员来说完全没有区别。你完全可以把 Java 所有的原始类型都想成引用类型,之后你能对它们做的事情,你的编程思路和方式,都不会因此有任何的改变。

    从这个角度来看,Java 在语义上是没有值类型的。值类型和引用类型如果同时并存,程序员必须能够在语义上感觉到它们的不同,然而不管原始类型是值类型还是引用类型,作为程序员,你无法感觉到任何的不同。所以你完全可以认为 Java 只有引用类型,把原始类型全都当成引用类型来用,虽然它们确实是用值实现的。

    一个在语义上有值类型的语言(比如 C#,Go 和 Swift)必须具有以下两种特性之一(或者两者都有),程序员才能感觉到值类型的存在:

    1. deref 操作。这使得你可以用 *x = 2 这样的语句来改变引用指向的内容,导致共享地址的其它引用看到新的值。你没法通过 x = 2 让其他值变量得到新的值,所以你感觉到值类型的存在。
    2. 像 struct 这样的“值组合类型”。你可以通过 x.foo = 2 这样的成员赋值改变引用数据(比如 class object)的一部分,使得共享地址的其它引用看到新的值。你没法通过成员赋值让另一个 struct 变量得到新的值,所以你感觉到值类型的存在。

    实际上,所有的数据都是引用类型就是 Scheme 和 Java 最初的设计原理。原始类型用值来传递数据只是一种性能优化(叫做 inlining),它对于程序员应该是透明(看不见)的。那些在面试时喜欢问“Java 是否所有数据都是引用”,然后当你回答“是”的时候纠正你说“int,boolean 是值类型”的人,都是本本主义者。

    思考题

    有人指出,Java 的引用类型可以是 null,而原始类型不行,所以引用类型和值类型还是有区别的。但是其实这并不能否认本文指出的观点,你可以想想这是为什么吗?

    3

    展开全文
  • JAVA值类型么?

    2016-06-12 09:35:49
    JAVA值类型么?原文链接:JAVA值类型么?Java值类型,原始类型 int,boolean 等是值类型,其实是长久以来的一种误解,它混淆了实现和语义的区别。不要以为 Java 的官方文档那样写就是权威定论,就可以说...

    JAVA 有值类型么?

    原文链接:JAVA 有值类型么?

    Java 有值类型,原始类型 int,boolean 等是值类型,其实是长久以来的一种误解,它混淆了实现和语义的区别。不要以为 Java 的官方文档那样写就是权威定论,就可以说“王垠不懂” :) 当你认为王垠不懂一个初级问题的时候,都需要三思,因为他可能是大智若愚…… 看了我下面的论述,也许你会发现自己应该怀疑的是,Java 的设计者到底有没有搞明白这个问题

    胡扯结束,现在来说正事。Java,Scheme 等语言的原始类型,比如 char,int,boolean,double 等,在“实现”上确实是通过值(而不是引用,或者叫指针)直接传递的,然而这完全是一种为了效率的优化(叫做 inlining)。这种优化对于程序员应该是不可见的。Java 继承了 Scheme/Lisp 的衣钵,它们在“语义”上其实是没有值类型的。

    这不是天方夜谭,为了理解这一点,你可以做一个很有意思的思维实验。现在你把 Java 里面所有的原始类型都“想象”成引用类型,也就是说,所有的 int, boolean 等原始类型的变量都不包含实际的数据,而是引用(或者叫指针),指向堆上分配的数据。然后你会发现这样“改造后”的 Java,仍然符合现有 Java 代码里能看到的一切现象。也就是说,原始类型被作为值类型还是引用类型,对于程序员完全没有区别。

    举个简单的例子,如果我们把 int 的实现变成完全的引用,然后来看这段代码:

    int x = 1;    // x指向内存地址A,内容是整数1
    int y = x;    // y指向同样的内存地址A,内容是整数1
    x = 2;        // x指向另一个内存地址B,内容是整数2。y仍然指向地址A,内容是1。
    

    由于我们改造后的 Java 里面 int 全部是引用,所以第一行定义的 x 并不包含一个整数,而是一个引用,它指向堆里分配的一块内存,这个空间的内容是整数 1。在第二行,我们定 int 变量 y,当然它也是一个引用,它的值跟 x 一样,所以 y 也指向同一个地址,里面的内容是同一个整数:1。在第三行,我们对 x 这个引用赋值。你会发现一个很有意思的现象,虽然 x 指向了 2,y 却仍然指向 1。对 x 赋值并没能改变 y 指向的内容,这种情况就跟 int 是值类型的时候一模一样!所以现在虽然 int 变量全部是引用,你却不能实现共享地址的引用能做的事情:对 x 进行某种操作,导致 y 指向的内容也发生改变。

    出现这个现象的原因是,虽然现在 int 成了引用类型,你却并不能对它进行引用类型所特有(而值类型没有)的操作。这样的操作包括:

    1. deref。就像 C 语言里的 * 操作符。
    2. 成员赋值。就像对 C struct 成员的 x.foo = 2 。

    在 Java 里,你没法写像 C 语言的 x = 2 这样的代码,因为 Java 没有提供 deref 操作符 。你也没法通过 x.foo = 2 这样的语句改变 x 所指向的内存数据(内容是1)的一部分,因为 int 是一个原始类型。最后你发现,你只能写 x = 2,也就是改变 x 这个引用本身的指向。x = 2 执行之后,原来数字 1 所在的内存空间并没有变成 2,只不过 x 指向了新的地址,那里装着数字 2 而已。指向 1 的其它引用变量比如 y,不会因为你进行了 x = 2 这个操作而看到 2,它们仍然看到原来那个1……

    在这种 int 是引用的 Java 里,你对 int 变量 x 能做的事情只有两种:

    1. 读出它的值。
    2. 对它进行赋值,使它指向另一个地方。

    这两种事情,就跟你能对值类型能做的两件事情没有区别。这就是为什么你没法通过对 x 的操作而改变 y 表示的值。所以不管 int 在实现上是传递值还是传递引用,它们在语义上都是等价的。也就是说,原始类型是值类型还是引用类型,对于程序员来说完全没有区别。你完全可以把 Java 所有的原始类型都想成引用类型,之后你能对它们做的事情,你的编程思路和方式,都不会因此有任何的改变。

    从这个角度来看,Java 在语义上是没有值类型的。值类型和引用类型如果同时并存,程序员必须能够在语义上感觉到它们的不同,然而不管原始类型是值类型还是引用类型,作为程序员,你无法感觉到任何的不同。所以你完全可以认为 Java 只有引用类型,把原始类型全都当成引用类型来用,虽然它们确实是用值实现的。

    一个在语义上有值类型的语言(比如 C#,Go 和 Swift)必须具有以下两种特性之一(或者两者都有),程序员才能感觉到值类型的存在:

    1. deref 操作。这使得你可以用 *x = 2 这样的语句来改变引用指向的内容,导致共享地址的其它引用看到新的值。你没法通过 x = 2 让其他值变量得到新的值,所以你感觉到值类型的存在。
    2. 像 struct 这样的“值组合类型”。你可以通过 x.foo = 2 这样的成员赋值改变引用数据(比如 class object)的一部分,使得共享地址的其它引用看到新的值。你没法通过成员赋值让另一个 struct 变量得到新的值,所以你感觉到值类型的存在。

    实际上,所有的数据都是引用类型就是 Scheme 和 Java 最初的设计原理。原始类型用值来传递数据只是一种性能优化(叫做 inlining),它对于程序员应该是透明(看不见)的。那些在面试时喜欢问“Java 是否所有数据都是引用”,然后当你回答“是”的时候纠正你说“int,boolean 是值类型”的人,都是本本主义者。

    个人总结: 本篇讲的是java语言语义上的问题,就java而言,语言在设计时语义上应该是没有值类型的,在具体实现时,考虑到性能,实现时会使用值类型的手法。从不同的方向看问题而已。

    展开全文
  • java中的参数传递(只有传递没有引用传递)

    万次阅读 多人点赞 2019-07-31 19:25:14
    Java中只有传值调用(传递),没有传址调用(址传递或者引用传递)。所以在java方法中改变参数的是不会改变原变量的的,但为什么改变引用变量的属性却可以呢?请看下面的解答。 java中的数据类型 Java中...

    Java中只有传值调用(值传递),没有传址调用(址传递或者引用传递)。所以在java方法中改变参数的值是不会改变原变量的值的,但为什么改变引用变量的属性值却可以呢?请看下面的解答。

    java中的数据类型

    Java中数据类型分为两大类:基本类型和引用类型。相应的,变量也分这两种类型:基本类型和引用类型。

    基本类型的变量保存原始值,即它代表的值就是数值本身;

    而引用类型的变量保存的值是引用值,"引用值"指向内存空间的地址,代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。

    基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress。

    引用类型包括:类、接口类型和数组。

    java中只有值传递

    在日常编码中,会经常看到如下现象:

    1、对于基本类型参数,在方法体内对参数进行重新赋值,不会改变原有变量的值。

    2、对于引用类型参数,在方法体内对参数进行重新赋予引用,不会改变原有变量所持有的引用。

    3、方法体内对参数进行运算,不会改变原有变量的值。

    4、对于引用类型参数,方法体内对参数所指向对象的属性进行操作,将改变原有变量所指向对象的属性值。

    举个例子:

    public class Main {
    
        private static void getMiddleOne(boolean b, Boolean boo, Boolean[] arr){
    
            b = true;
    
            boo = new Boolean(true);
    
            arr[0] = true;
    
        }
    
           //测试
    
        public static void main(String[] args) {
    
            boolean b = false;
    
            Boolean boo = new Boolean(false);
    
            Boolean[] arr = new Boolean[]{false};
    
            getMiddleOne(b, boo, arr);
    
            System.out.println(b);
    
            System.out.println(boo.toString());
    
            System.out.println(arr[0]);
    
            /**
    
            * output:
    
            * false
    
            * false
    
            * true
    
            */
    
        }
    
    }

    我们只要了解了下面两点就可以解答上面的现象了:

    1、基本数据类型的值就是数值本身,所以示例中的b的值就是false;包装类因为会自动装箱拆箱,所以可以和基本类型一样处理,所以示例中boo的值就是false;数组是引用类型,所以arr的值就是指向该Boolean[]的引用。

    2、java中只有值传递没有引用传递,所以传入getMiddleOne方法的三个参数分别是b的值拷贝, boo的值拷贝, arr的值拷贝。

    通过上面两点就可以清楚了,getMiddleOne方法中执行的 b=true 和 boo = new Boolean(true) 都是把新值赋给了他们的拷贝,所以不改变原变量的值;同样,arr[0] = true 是把true复制给了arr的拷贝所指向的数组的第一个元素,arr的值和arr的拷贝的值都是该数组的引用,所以arr的拷贝所指向的数组和arr所指向的数组是同一个,所以改变arr的拷贝的数组的元素会同样影响到原变量arr。

    总结

    java中只有值传递,基本类型传递的是值的副本,引用类型传递的是引用的副本。

     

    展开全文
  • Java中引用类型和值类型的不同

    千次阅读 2016-08-13 22:06:33
    Java编程过程中,经常有人会因为没有弄清楚引用类型与值类型的区别而导致各种稀奇古怪的Bug出现,而且出现了还不知道问题在哪里。这里将简单阐述一下两者的区别。 引用数据类型:该类型指向一个对象,而不是原始...

    在Java编程过程中,经常有人会因为没有弄清楚引用类型与值类型的区别而导致各种稀奇古怪的Bug出现,而且出现了还不知道问题在哪里。这里将简单阐述一下两者的区别。

    引用数据类型:该类型指向一个对象,而不是原始值。

    这个如何理解呢?学过C或者C++的同学肯定知道指针这个东西,那么引用类型也可以类型的理解。就是说将该类型传给某个方法的时候,在该方法里面进行操作其实是该引用类型所指向的那片内存所在的数据。就好比同时将c引用类型变量传给了A方法和B方法,先让A方法改变c指向的对象的某个属性d,然后让B方法将c对象的属性d打印出来,会发现,这个d的值是在A方法里面修改的一样。

    引用数据类型举例:类以及类创建的对象,数组,接口等等。


    值类型:其实就是基本数据类型,如:数值型的有int,byte,long等,字符型有:char, 还有布尔类型:boolean。这种类型的数据变量在声明之后JVM会立刻为其分配内存空间。而不是像引用类型那样,将该变量又指向某个值所在的空间。                                                          

                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

    在Java编程过程中,经常有人会因为没有弄清楚引用类型与值类型的区别而导致各种稀奇古怪的Bug出现,而且出现了还不知道问题在哪里。这里将简单阐述一下两者的区别。

    引用数据类型:该类型指向一个对象,而不是原始值。

    这个如何理解呢?学过C或者C++的同学肯定知道指针这个东西,那么引用类型也可以类型的理解。就是说将该类型传给某个方法的时候,在该方法里面进行操作其实是该引用类型所指向的那片内存所在的数据。就好比同时将c引用类型变量传给了A方法和B方法,先让A方法改变c指向的对象的某个属性d,然后让B方法将c对象的属性d打印出来,会发现,这个d的值是在A方法里面修改的一样。

    引用数据类型举例:类以及类创建的对象,数组,接口等等。


    值类型:其实就是基本数据类型,如:数值型的有int,byte,long等,字符型有:char, 还有布尔类型:boolean。这种类型的数据变量在声明之后JVM会立刻为其分配内存空间。而不是像引用类型那样,将该变量又指向某个值所在的空间。

    这个应该比较好理解了,也就是该类型的变量就是实实在在的原始数据了。要注意的是,和上面的例子相反,如果这时c是属于值类型的数据,那么将其传给A方法进行处理,然后在B方法打印c的话,此时会发现,c的值是没有改变的。

    学过C或者C++的同学用传值和传址去理解,也是一样的。

    这里特别指出,Java里面的集合经常有人会踩到的坑。例如List的add()和addAll()方法,当你进去的数据是引用类型数据的话,那么,无论你将该数据类型传到了多少个add()方法,用add过该引用数据类型的List去get()到的数据,都是同一个。那么这时候要注意了,你一旦将其修改,其实是修改了原始数据了。这个要特别注意,别以为你新建了一个集合,将数据add进去,然后用该新建的集合取出来的就是新的对象了,其实不是的,还是原来那个数据。

    好了,关于Java的基本数据类型和引用数据类型的区别分析到这里,有什么不当的地方,望大神不吝指出。PS:这是第一次在CSDN发表博客,感觉写到很low。。。

    展开全文
  • java类型和范围

    2018-06-28 10:58:45
    java类型中存在三种类型,分别为:简单类型,引用类型以及空类型。同样对应的有三种数据,简单,引用以及null。其中null是一种特殊的类型。由于null的类型没有名字,所以不能声明一个变量是空类型的,同样不...
  • 很多书上说,java只有传递,所以没有引用传递,这句话是对的么? 上代码   例一: public class StringTest { public int i=1; public static void st(StringTest st){ st.i=10; } public static ...
  • 没有值的json字符串Given a string and some of the primitive data type values, we have to concatenate them with the string in Java. 给定一个字符串和一些原始数据类型值,我们必须将它们与Java中的字符串...
  • 栈中有基本类型和对象句柄,基本类型又叫值类型,对象句柄又叫引用类型;java中的方法值中传值时,就有了值传递和引用传递。 基本类型 Java中有8中基本类型,int, short, long, byte, float, double, boolean, ...
  • 根据Oracle最新的PPT ...总所周知,primitive类型(值类型)是放在栈里,速度快,对象是放在堆了,所有的都放在堆里,速度就变慢了,那么java要退出游戏编程吗?要退出低端工控编程吗?我正想研究Arduino,原来以为它
  • java中的参数传递不管是基本数据类型还是引用 数据类型只有传递,没有引用传递,也就是说Java中参数传递的是内容,而不是引用。下面看几个例子。 例1: /* 1. 基本数据类型的参数传递 2. */ package ch.demo2; ...
  • 记录下的注意的地方: 1、整数类型:整数类型大小依次分别为:byte,short,int,long;...java整数类型中默认是int,因此long的写法是long i=12233344l; 2、小数类型(浮点型):float(单精度):可以表示8位
  • Java的只有传递,没有引用传递。 在Java中,参数都是按传递的。被传递到方法中的拷贝,要不就是一个引用或一个变量,取决于原始参数的类型。 参数类型有四种: 基本数据类型、包装类(属于immunable不可变对象...
  • Java 基本数据类型初始(默认值)

    千次阅读 2017-05-15 10:41:57
    若基本数据类型作为类成员的时候,即使没有进行初始化。Java也会给定默认的初始。默认是如下所示: boolean false char '/uoooo'(null) byte (byte)0 short (short)0 int 0 long 
  • 也就是说,java规范中,没有明确指出boolean的大小。 存在3种说法: 1、1个bit(1/8个字节)理由:boolean类型只有true和false两种逻辑,在编译后会使用1和0来表示,这两个数在内存中按位...
  • 转载: ...java核心技术卷I里有一个结论我觉得挺有意思的:java没有引用传递,只有传递 首先看定义: 传递,是指方法接收的是调用者提供的 引用传递,是指方法接收的是调用者提供的变...
  • 其中stageNo是通过传参Integer stageNo进来,loanPrj.getStageNo()返回的也是Integer,这样就有问题了,,==在java中,比较的是栈中存放的,对于对象类型,运行栈中存放的是指向对象的地址...
  • 一直认为java 中String类型不能修改,主要是因为String是final的,而且里面没有设置的set方法。但是可以通过反射机制改变。 例1 public class Test { public static void main(String[] args) throws ...
  • 此种问题答案: 遇到超过最大的情况,转换为二进制进行相加,加完后再进行10进制转换就行。...超过最大的求,在我看来这种情况没有多大的实际使用意义,更多时候程序发现异常时候能够理解是哪儿出了问题。 ...
  • 若基本数据类型作为类成员的时候,即使没有进行初始化。java也会给定默认的初始。默认是如下所示: boolean false char '/uoooo'(null) byte (byte)0 short (short)0 int 0 long 
  • java代码走SQL没有值,但是复制到sql语句到工具中直接查询有值。最后发现是日期格式的问题:数据库是date类型('2018-02-14')的 而我传入的值是'2018-05-30 00:00:00'这种格式查询不到,将传入的值格式化一下DATE_...
  • Java传递

    2021-01-29 00:27:35
    Java中方法到底是传递还是引用传递? 理解3张图 传递:调用方法时,传入的实参是...低水平的博客作者:Java基本数据类型传递,引用数据类型是引用传递 大错特错的 实际上Java只有一种传递,只有传递 ...
  • Java只有传递

    2021-01-02 04:12:21
    这个没有误解,先撇开基本类型值传递不谈了。 思考 如果参数是引用类型,就是引用传递?这句话对吗? 很多人以为传递的是对象的引用所以是引用传递,理解的误区就在这 先抛出结论 Java 没有引用传递 官方说的 而且...
  • Java String类型值真的不可改变吗?

    千次阅读 2015-03-31 00:40:09
    一直认为java 中String类型不能修改,主要是因为String是final的,而且里面没有设置的set方法。但是可以通过反射机制改变。 例1 public class Test { public static void main(String[] args) throws ...
  • 结合上面的分析,关于传递和引用传递可以得出这样的结论:(1)基本数据类型传值,对形参的修改不会影响实参; (2)引用类型传引用,形参和实参指向同一个...“在Java里面参数传递都是按传递”这句话的意思是:
  • java传递

    2021-04-28 11:05:40
    java只有传递,没有引用传递 什么是传递? 传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 重点是“复制” 什么是引用传递...
  • Java Int 数据类型,非初始化时的

    千次阅读 2017-07-03 20:00:13
    为 0 而不是 null ...因为我们直觉上会认为没有初始化,那么应该是 null 才对。 所以 有二个解决办法: 一, 使用 Integer 类型 二,Int 类型的业务定义 0 时是无意义的。 这样就不会出错了。
  • java常用数据类型

    2016-02-01 23:29:00
    java的数据类型 java没有无符号类型的数值类型 基本类型 大小 最小值 最大 包装器类型 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,350
精华内容 2,540
关键字:

java没有值类型

java 订阅