(传值还是传引用,==和equals比较,String创建)内存语义

传值,传引用

问题引入

先给出一个示例,你看看自己能不能做出来,对传值,传引用掌握了没有.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SendDataTest
{
public static void main(String[] args)
{
List<String> list = new ArrayList<>();
list.add("a");
final List<String> list2 = new ArrayList<>();
list2.add("a2");
int i = 1;
Integer integer = 1;
integer = 2;
String str = new String("string1");
int[] nums = {1, 2, 3};

change(list, list2, i, integer, str, nums);

System.out.println(list);
System.out.println(list2);
System.out.println(i);
System.out.println(integer);
System.out.println(str);
System.out.println(Arrays.toString(nums));
}

static void change(List<String> list, List<String> list2,
int i, Integer integer, String str, int[] nums)
{
list.add("b");
list2.add("b2");
i = 2;
integer = 10;
str = new String("string2");
int[] nums2 = {4, 5, 6};
nums = nums2;
}
}
点击查看运行结果(如果你不思考直接查看,将失去它的意义)
1
2
3
4
5
6
[a, b]
[a2, b2]
1
2
string1
[1, 2, 3]

如果你有出错的地方,或你不清楚自己为什么这样答的话,就需要跟着我的思路来梳理一下.

  1. 首先你要明白问题中传值和传引用是什么意思?
    传值:基本数据类型的传递
    传引用:引用数据类型的传递

8大基本数据类型:byte,int,short,long,double,float,boolean,char
引用类型:数组,类,接口

所以,变量可分为基本数据类型变量引用数据类型变量,
基本类型变量:直接把数据放入变量的储存单元里.
(例如int i=5;这里就直接声明int基本数据类型变量,变量名称为i,把数据5直接放入名称为i的存储单元中)
引用类型变量:存储单元里面保存的不是数据对象,而是数据对象的引用地址,真正的数据对象是放在堆内存中的.
(例如List<String> list=new ArrayList<>();,这时就先声明一个List<String>的引用类型变量,
引用变量的名称为list,同时开辟了一个堆内存空间,给这块内存地址分配了一个值放入list变量的储存单元中,创建ArrayList对象放入堆内存中)

传递是通过变量之间的赋值实现的:(单从变量角度看的话,变量之间的传递是值传递.因为无论是值传递,还是引用传递,其传递的原理都是传递变量储存单元中的数据复制一份放在接收变量储存单元中)

传值

把上面的一部分抽取出来,

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args)
{
int i = 1;// 1

change(i);// 2

System.out.println(i);//5
}

static void change(int n)//3
{
n = 2;// 4
}

按照单线程执行顺序,我进行了步骤标注,下面按照步骤顺序来讲解:

  1. 声明基本数据类型变量,变量名称为i,储存单元里放入数据1
  2. 进入方法change,传入参数变量i
  3. 声明基本数据类型变量,变量名称为n,此时变量n储存单元里面的数据就是步骤2中变量i中的储存单元数据(储存单元数据复制),所以此时变量n的存储单元中数据是1
  4. 把变量n中的储存单元数据改为2(修改的是变量n中储存单元的数据,并不影响变量i中储存单元的数据)
  5. 控制台打印变量i,输出变量i中储存单元的值还是1

这样说你懂了吗?
还要记住的是,变量只在当前所在的{}大括号中有效,不同{}大括号中可以有不同的变量名称
为了方便解释,我们说上面有两个方法栈内存,分别是main方法栈change方法栈,他们两个变量名称也可以相同.(我是为了做区分,命名了不同的变量名称)
在下面贴出步骤4执行前,和执行后的内存语义.(jvm中可以有多个线程栈,这里只有main线程栈.)
在这里插入图片描述
在这里插入图片描述

传引用

示例1:改变接收变量所指向的数据对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args)
{
List<String> list = new ArrayList<>();// 1
list.add("a");// 2

change(list);// 3

System.out.println(list);// 6
}

static void change(List<String> list)// 4
{
list.add("b");// 5
}

同理我依然对步骤进行了标注

  1. 声明List<String>(泛型为String的List接口)引用数据类型变量,变量名称为list,因为是引用数据类型变量,在堆内存中分配一块内存空间用来存放数据对象,有一个值用来标注这块堆内存空间在堆内存中的位置,通过这个值访问到堆内存空间中的数据对象,就像通过下标获取数组值一样,我们称这个值为数据对象在堆内存中的地址,再把这个值放入引用变量list的储存单元中,所以这个引用变量就可以通过这个值访问到数据对象,我们通常简称这个值为引用地址.
  2. 因为list是引用变量,而不是基本变量.所以要通过引用变量list储存单元中的引用地址找到数据对象,并调用数据对象的add方法,添加数据”a”
  3. 进入change方法,传入参数为:引用变量list,同值传递一样,此时传入的也是储存单元中的值,
  4. 声明引用变量,名称为list,注意,这里的变量名称是储存在change方法栈中的,所以不会和main方法栈中的list变量造成冲突,同值传递一样,把main方法栈中的list变量储存单元中的值传递给change方法栈中的list变量,也就是说两个方法栈中的引用变量list都指向了同样的数据对象.
  5. 由上一步可得,两个方法栈中的list变量指向了同样的数据对象,此时往数据对象中添加数据”b”,两个方法栈中的list变量所指向的数据对象也是同样进行了改变.
  6. 打印数据对象,输出[a, b].

同样在步骤5执行前,和执行后贴出内存语义.
在这里插入图片描述
在这里插入图片描述

示例1总结:改变的是数据对象,而并未改变两个变量的引用地址

示例2:把接收变量指向新的数据对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args)
{
List<String> list = new ArrayList<>();// 1
list.add("a");// 2

change(list);// 3

System.out.println(list);// 8
}

static void change(List<String> list)// 4
{
List<String> list2 = new ArrayList<>();// 5
list2.add("b");// 6
list = list2;// 7
}

注意这里与示例一的不同点,我只对change方法进行了改变,重新标注了步骤顺序,这里省略了1-4步骤
5. 同步骤1一样,在change方法栈中创建引用变量list2,并分配内存空间放入ArrayList数据对象,此时list2的储存单元中是新数据对象的引用地址,因为是新的对象,所以有新的引用地址,即不同于这两个方法栈中的list变量的引用地址
6. 根据list2中的引用地址找到新建的数据对象,往里面添加数据”b”(注意:此时第一个建的数据对象并未发生改变,还是只保存了数据”a”)
7. 把change方法栈list变量储存单元的引用地址改为change方法栈中的list2变量的引用地址,即change方法栈中的list变量指向新建的数据对象.(注意:此时并没有改变main方法栈中引用变量list储存单元中的引用地址,它还是指向原来的对象)
8. 由步骤7,8可知,main方法栈list变量的引用地址并未改变(还指向原对象),原对象也未改变,所以控制台输出[a]

示例2总结:改变的是change方法栈list变量的引用地址,使之指向新创建的对象,而main方法栈中的list变量未曾改变.

在上面讲值传递 示例1 示例2中,我反复说是哪个方法栈的那个变量,是引用变量,还是基本变量,改变的是储存单元的值还是改变数据对象,
只有清楚了这些才能知道,其为什么不能改变.

示例3:final是为了让储存单元中的值不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args)
{
final List<String> list = new ArrayList<>();
list.add("a");

change(list);

System.out.println(list);
}

private static void change(List<String> list)
{
list.add("b");
}

因为final修饰变量,是修饰变量储存单元中的数据不能改变,这里也就是不能改变list中保存的引用地址,也就是list会一直指向一个数据对象而不能指向其他数据对象.
因为两个list变量都指向那个数据对象,list.add("b");修改的是那个数据对象,所以会输出[a,b]

示例4:String类.

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args)
{
String str = "a";
change(str);
System.out.println(str);
}

private static void change(String str)
{
str = "b";
}

我们在讲解这个,应该先查看后面的String内存语义的讲解.
因为String不通过new创建的数据对象会放入String常量池中,而通过new创建的会放入堆内存中,
所以这里str = "b";会在String常量池中新建一个”b”数据对象,同时生成一个新的引用地址,并把这个引用地址放入change方法栈中的list变量储存单元中.
而这时change方法栈中的list变量就会指向String常量池中的”b”数据对象.

示例5:Integer类.

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args)
{
Integer i = 1;
change(i);
System.out.println(i);
}

private static void change(Integer i)
{
i = 2;
}

在下面我们也讲解了Integer的内存语义.
如果不通过new创建新的Integer对象的话,如果范围在[-128,127]就去Integer内部类中cache变量(Integer对象数据)中找到对应的Integer对象,
如果不在就在堆内存new一个Integer数据对象,并把引用地址放入对应Integer变量的储存单元中.

所以说传递变量list指向的是cache中值为1Integer对象,在执行i = 2;之前,接收变量list指向的是cache中值为1Integer对象,也就是指向同一个对象,两个list变量储存单元中保存的数据是相同的.(因为change方法中list变量储存单元中的值,是main中list传递过来的)

Integer的==和equals比较

Integer,int,long的比较问题,先讲懂了这个,也就是知道String的比较了.

Integer与Integer比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);

Integer i5 = -128;
Integer i6 = -128;
System.out.println(i5 == i6);
Integer i7 = -129;
Integer i8 = -129;
System.out.println(i7 == i8);
System.out.println();
点击查看运行结果
1
2
3
4
true
false
true
false

注意Integer i1 = 127;创建的时候会调用Integer类的valueof方法,
把数据int基本数据类型127封装成Integer对象,并把这个对象的引用地址放入i1变量的储存单元中.

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

这里要想看懂,就必须了解IntegerCache到底是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

这里我们知道了它是Integer的私有静态内部类,所以在Integer的类中可以随意调用.
我们知道cache变量是Integer类型的数据,而low=-128,那么high变量到底是什么?
上面我们可以看出,要想知道high变量,就要看懂静态的代码块,
要想知道代码块,就要知道那个if方法到底是true,还是false.下面我们进行测试

1
2
3
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
System.out.println(integerCacheHighPropValue!=null);// false

经过我的测试,它是为false的,下面我们把这个静态代码块来简化一下,省去无必要的代码.

1
2
3
4
5
6
7
8
static {
high = 127;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}

注意,这里我把这段静态代码块简化了,因为if为false,而且在我们使用的时候,如果省略了assert,
因为在我们JVM不指定-ea的时候,它是不影响程序的.
我们可以得到总结,这是一个cache引用变量数据,数组中放的都是Integer对象,
下标0~255依次对应的是-128~127,这里我们再回过头来看IntegervalueOf方法.

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

从上面分析,我们知道了low = -128,high = 127,cache是一个Integer数组.
我们可以看出,如果你设置的int值在[-128,127]这个范围,就不会创建新的对象,直接去cache引用变量中去拿所对应的Integer对象.
如果我们不在这个范围,就会重新创建一个Integer对象.

下面我们看看创建Integer对象的构造函数.

1
2
3
4
5
private final int value;

public Integer(int value) {
this.value = value;
}

我们可以看到,Integer对象内部是由一个int基本数据类型变量value来维护的,而这个valuefinal修饰的,
也就是说这个变量的储存单元的值是不可变的.它存放的数据就是我们在构造函数中初始化的值.

到这里我们就分析完毕了,所以我们再回过头来分析一下开头的那个示例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);// 1
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);// 2

Integer i5 = -128;
Integer i6 = -128;
System.out.println(i5 == i6);// 3
Integer i7 = -129;
Integer i8 = -129;
System.out.println(i7 == i8);// 4
System.out.println();

输出1:==比较的是储存单元中的值,127在[-128,127]这个范围,所以它在初始化调用valueOf方法时,不会创建新的对象,,i1i2引用变量都是指向了cache数组中value为127的Integer对象.
指向了同一个对象,储存单元中保存的引用地址也是相同的,所以为true.
输出2:因为128不在[-128,127]的这个范围,所以它在初始化调用valueOf方法时,会创建新的对象,他们两个都创建了新的对象.
虽然这两个Integer对象的value是相同的,但是他们两个对象在堆内存中所占的空间位置是不同的,他们各有各的引用地址,所有i3i4的储存单元中是不同的引用地址,所有为false.
输出3 输出4同理可得.

我们再来分析下面这种情况

1
2
3
Integer i1 = 127;
Integer i2 = new Integer(127);
System.out.println(i1 == i2);
点击查看执行结果
1
false

如果你还是做错的话,或不清楚为什么输出false,就说明,你对上面我的讲解还是理解不够到位,下面我们来分析这种情况的原因.
首先你要搞清楚new的关键字,它是表示新建一个数据对象,并在堆内存中分析一块新的空间,并且这个数据对象有一个新的引用地址.
所以,你搞清楚了new的关键字的内存语义,我们就可以分析出,
i1变量的引用地址指向的是Integer内部类的cache数组中的value为127的Integer对象,而i2指向的是通过new关键字新建在堆内存中的Integer对象.
虽然i1i2变量指向数据对象value变量的储存单元中的数据是同样的,都是127; 但是==比较的是i1i2两个变量中储存单元中储存的值,也就是两个不同的引用地址(因为他们指向的是堆内存空间中两个不同位置的数据对象)

  • 两个对象==比较的总结
    所以,要想比较两个对象的==是否为true,关键点是在于两个引用变量储存单元中的值是否相同,也就是引用地址是否相同,也就是是否指向了同一个对象,是否指向同一个对象的关键点在于是不是new了一个新的对象.

Integer与基本数据类型比较

分析完了,两个Integer对象的==比较,下面我们再来分析Integerint long==比较

1
2
3
4
5
6
Integer i1 = 127;
int i2=127;
long i3=127;
System.out.println(i1 == i2);// 1
System.out.println(i1 == i3);// 2
System.out.println(i2 == i3);// 3
点击查看运行结果
1
2
3
true
true
true

如果你回答错了,也没关系,下面通过我的分析你就可以知道为什么了.
首先你要了解Integer对其他基本数据类型变量进行==比较,或直接直接进行数据比较,例如i1 == 5
这种情况会默认调用intValue方法,

1
public int intValue() { return value; }

可以看出它会返回一个int值,也就是返回一个基本数据类型的变量,其返回的就是Integer内部维护的value变量,在上面我们分析出了value变量是初始化时指定的.
所以输出1进行==比较的就是:i1变量指向的Integer对象中value变量储存单元的数据是否与i2变量储存单元的数据相同,我们可以知道它们是相同的,都是指向了127.
输出2同理我们可以得出结果也为true.
输出3更为简单,因为i2i3是基本数据类型的变量,所以它们储存单元中保存的都是数据127,所以为true.

基本数据类型与基本数据类型比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int i1 = 5;
long i2 = 5L;
double i3 = 5D;
double i4 = 5.0;
double i5 = 5.1D;
float i6 = 5.0F;
float i7 = (float) 5.0;
System.out.println(i1 == i2);// 1
System.out.println(i1 == i3);// 2
System.out.println(i1 == i4);// 3
System.out.println(i1 == i5);// 4
System.out.println(i1 == i6);// 5
System.out.println();
System.out.println(i2 == i3);// 6
System.out.println(i2 == i4);// 7
System.out.println(i4 == i7);// 8
点击查看运行结果
1
2
3
4
5
6
7
8
9
true
true
true
false
true

true
true
true

至于结果为什么,下面我们做一些讲解.
根据我知道的,直接写数据5可以赋值给int long double float变量.
如果是5.0只能赋值给double(因为它默认是double类型),如果想赋值给float必须加(float)强制转换,或在数字后面加f F例如5.0f5.0F
long类型后面可以写l L,如果赋值的数据超出了int的范围,就需要指定字母
double类型后面可以写d D
float类型后面可以写f F

这里的5.0可以默认更换为5,如果是5.00000默认也是转换成5放入基本数据类型变量的储存空间中.
因为它们后面的0是可以忽略的,可以更多的节省空间.

Integer与Integer,基本数据类型的equals比较

我们分析完了Integer==比较,我们再来分析equals比较.
有了前面我们对Integer类的铺垫,下面我们先来看equals方法,再来讲解示例.

1
2
3
4
5
6
7
8
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}

public int intValue() { return value; }

我们要想知道equals方法,就要知道obj instanceof Integer的意思.
它表示传入的参数如果是Integer类型,结果就为true,否则为false.
如果传入的也是Integer类型的话,就让这两个Integer对象来比较其内部的value变量,看看这两个变量储存单元中的值是否相同.相同为true,不相同为false.
如果不是Integer类型的话,就直接返回false.

1
2
3
4
5
6
7
Integer i1 = 127;
Integer i2 = new Integer(127);
int i3 = 127;
long i4 = 128;
System.out.println(i1.equals(i2));// 1
System.out.println(i1.equals(i3));// 2
System.out.println(i1.equals(i4));// 3
点击查看运行结果
1
2
3
true
true
false

如果你猜错了输出1的结果,那是不可原谅的,因为在上面我们说的很清楚,如果是两个Integer对象的话,就比较他们的value变量中的储存单元中的值是否相同,如果传入的不是Integer对象就直接返回false.
如果你猜错了输出2,3,没关系.
那是因为**Integer对象在与基本数据类型变量()进行equals比较的时候,会把基本数据类型的变量转换为对象.
这里的输出2就是把i3基本变量的储存单元中的值,也就是127通过Integer类的valueOf方法转换成了Integer对象,然后再if判断为true,再进行==比较两个Integer对象的value.
为什么会调用IntegervalueOf方法,是因为i3变量是int类型,而int类型对应的类就是Integer.
如果直接使用i1.equals(127)结果也是true,因为它会调用IntegervalueOf.
如果是使用i1.equals(127L)结果就为false了,因为它会调用LongvalueOf,它会返回一个Long对象,因为if判断为false,直接返回false.而不会比较内部value变量储存单元中的数据.
输出3就是因为i4long类型的,所以会调用LongvalueOf进行转换为Long对象.所以为false.

总结

到这里Integer类型就讲述完毕了,下面进行了一个总结:
==比较:
(1)如果两个都是Integer对象就比较他们的变量的引用地址(这里需要注意new是创建一个新的对象,并分配一个新的引用地址,也就是说,在两个都是Integer对象时,有一个是通过new来创建的,结果就为false)
(2)只有一个是Integer对象,另外一个是基本数据类型,就比较Integer对象的value变量储存单元中的数据与后者储存单元中的值进行比较是否相同
(3)如果都是基本数据类型,就直接比较他们的储存单元中的值是否相同.
equals()比较:
(1)后者是Integer对象,比较它们的value变量储存单元中的数据是否相同.
(2)后者是基本数据类型,把基本数据类型的数据转换为对象,然后再进行比较.
如果后者是int类型,就调用IntegervalueOf方法.
如果后者是long类型,就调用LongvalueOf方法 …

String

在上面我们讲解了Integer类型的比较,下面我们来进行String的讲解.

==

老样子,我们先做个示例,再进行讲解为什么.

1
2
3
4
5
6
7
String a1 = "a";// 1
String a2 = "a";// 2
String a3 = new String("a");//3
String a4 = new String("a");// 4
System.out.println(a1 == a2);// 5
System.out.println(a1 == a3);// 6
System.out.println(a3 == a4);// 7
点击查看运行结果
1
2
3
true
false
false

在分析之前,我们要知道==比较的是两个变量储存单元中的数据是否相同.(这个在之前已经讲到)
String类型的变量是引用变量,而引用变量是通过引用地址指向堆内存中的一个数据对象.
步骤1没有通过new来创建新的数据对象,是因为步骤1先去一个叫String常量池的地方找是否有相同的数据对象,
如果有就直接把那个数据对象的引用地址放到变量的储存单元中,如果没有就在String常量池中新进一个数据对象.
所以,步骤1就在String常量池中新建了一个数据对象”a”,而步骤2就去String常量池中找是否有”a”数据对象,
这时找到了为”a”的数据对象,就把String常量池中数据对象”a”的引用地址放入a2变量的储存单元中,
这时a1a2的储存单元中放入的就是相同的数据(引用地址),所以他们指向的都是String常量池中为”a”的数据对象,所以步骤5输出true.

步骤3步骤4都是通过new来创建的数据对象,他们都在堆内存中分配了一块内存空间,有着不同的引用地址,他们指向的数据对象,也是堆内存中不同的数据对象.
所以,步骤6步骤7输出都为false.

equals

1
2
3
4
5
String a1 = "a";
String a2 = "a";
String a3 = new String("a");
System.out.println(a1.equals(a2));// 1
System.out.println(a1.equals(a3));// 2

我们先来看String类的equals方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private final char value[];

public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

我们可以看出如果==比较为true,他们的equals也一定返回true.
如果==为false,表示两个引用变量的储存单元中的数据(引用地址)不同,也就是指向了不同的数据对象.
先判断后者是否为String类型,如果不是直接返回false,双引号的字符串默认是String类型的,所以为true.
先把第二个变量转换为String对象,如果两个字符串的长度不相同的话,也返回false,那是一定的,
如果长度相同,再比较数据对象中的value数组,也就是char数组中的值是否相同,不相同也返回false,相同才能为true.

到这里,String类型的比较也讲解完毕了,下面我们做个总结.

总结

==比较比较的是两个引用地址是否相同(注意点就是创建对象的方式,有一个为new创建的就为false,否则为true)
equals比较如果==比较为true,equals比较也一定为true.
如果==为false,那就查看后者是否为String类型,不是为false,是的话再把后者强转为String对象,最后比较两个String对象中的char数组中的数据是否一致.