Hike News
Hike News

Java集合(二)-Set集合

Set集合

Set集合和Collection基本相同,没有提供额外的方法,主要是行为上的不同,Set不允许包含重复元素,否则add()方法会返回false。接下来将主要介绍四种Set类,HashSetTreeSetLinkedHashSetEnumSet四种。

HashSet类

HashSet使用hash算法来储存集合中的元素,具有很好的查找和存取性能,它的特点如下:

  1. 不能保证元素的顺序,可能变化
  2. HashSet不是同步的,如果多个线程同时访问并修改一个HashSet,则必须保证其同步。
  3. 集合的元素可以是null

当向HashSet集合中存入一个元素时,HashSet会调用对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的位置。但是如果两个元素通过equals()方法返回true,而它们的hahCode()方法返回值不相等,HashSet将会把他们储存在不同的位置,依然可以添加成功。也就是说,HashSet集合判断元素相等的标准是通过equals方法比较相等,并且两个对象的hashCode值一样。

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
Class A{
public boolean equals(Object obj){
return true;
}
}
Class B{
public int hashCode(){
return 1;
}
}
Class C{
public int hashCode(){
return 2;
}
public boolean equals(Object obj){
return true;
}
}
public Class HashSet{
public static void main(String[] args){
HashSet books = new HashSet();
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
}
}//添加两A,B,C对象
1
[B@1 ,B@1 ,C@2 ,A@54d ,A@8773f2]

从上面看出,即使A对象equals方法返回true,依然被当作两个对象;即使两个B对象hashCode方法返回值一样,但HashSet集合储存了两个hash值一样的对象。如果equals返回true,但是会放在不同的地方,不太好。如果hash值一样,但equals返回的false也麻烦了,集合会尝试保存把对象保存在同一个位置,并采用链式结构来保存多个对象,这样会导致利用哈希值查找的时候,性能下降。

所以总结一句:如果需要把对象保存到HashSet集合中去,重写类的equalshashCode方法,保证equals返回true时,hashCode返回值一样。

HashSet中每个能储存元素的位置通常称为桶(bucket),如果有多个元素的哈希值相同,但它们通过equals方法返回false,就需要在一个桶内放多个元素,然而这样会导致性能下降。

重写hashCode方法步骤
  1. 把对象内的每个参与equals方法比较的实例变量计算处一个int类型的哈希值
实例变量类型 计算方式 实例变量类型 计算方式
boolean hashCode = (f ? 0 : 1) float hashCode = Float.floatToIntBits(f)
整型byte、char、short、int hashCode = (int)f double long L = Double.doubleToLongBits(f);
hashCode = (int)(L^(L>>>32));
Long hashCode = (int)(f^(f>>>32)); 引用 hashCode = f.hashCode();

2.用第一步计算出的多个哈希值组合计算出一个哈希值返回,例如:

1
return f1.hashCode() + (int)f2;//f1,f2为实例变量

为避免直接相加产生的偶然,可以为各实例变量的哈希值乘以任意质数后再相加。但还有一点需要了解的是,这样并不能完全保证之后就不会产生两个相同的对象,向HashSet中添加可变对象后,后面修改了可变对象的实例变量,可能导致它和集合中其他元素相同,甚至不能正确访问(储存在不同位置,hash值不一样)。

LinkedHashSet

HashSet还有一个子类LinkedHashSet,同样也是根据元素的hashCode值来决定元素的储存位置,不同的是它使用链表维护元素的次序,这样可以以插入顺序保存元素。LinkedHashSet需要链表维护次序,所以性能略低于HashSet,但优势在于遍历集合内元素上。另外注意一点,它还是Set,所以依然不允许元素重复!

1
2
3
4
5
LinkedHashSet names = new LinkedHashSet();
names.add("James");
names.add("Jodan");
System.out.println(names);
//James,Jodan

TreeSet(SortedSet的实现类)

既然是实现类,那SortedSet就是TreeSet的接口了,顾名思义,TreeSet可以让元素处于一定次序状态。与HashSet相比,TreeSet额外提供了如下方法:

  • Comparator comparator():如果TreeSet采用定制排序,则返回定制排序所使用的Comparator,如果采用自然排序,则返回null
  • Object first():返回第一个元素
  • Object last():返回最后一个元素
  • Object lower(Object e):返回处于指定元素(任意元素,不需要在集合中)之前的元素
  • Object higner(Object e):返回处于指定元素之后(更大的元素)的元素
  • SortedSet subSet(Object e1,Object e2):返回从e1到e2的子集合(不包含e1,e2)
  • SortedSet headSet(Object max):返回子集合,元素都小于max
  • SortedSet tailSet(Object min):返回子集合,元素大于等于min

综上,大概就是提供了访问第一个、最后一个、前一个、后一个元素的方法,并提供了三个截取子集合的方法。与此同时,TreeSet采用红黑树的数据结构来储存数据,默认情况下使用自然排序。它的排序规则有以下两种:

1.自然排序

TreeSet调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种就是自然排序。Java提供了Comparable接口,该接口内定义了一个compareTo(Object o)方法,方法返回一个整数。实现接口就必须实现该方法。当一个对象之间进行比较时,例如:obj1.compareTo(obj2),如果返回0,则表明这两个对象相等;如果返回一个正整数,则obj1大于obj2;如果返回一个负整数,则obj1小于obj2。

下面提供了实现Comparable接口的类,并提供了比较大小的标准。

  • BigDecimal、BigInteger以及所有数值类型对应的包装类:按数值大小比较
  • Character:按字符的Unicode进行比较
  • Boolean:true对应的包装类实例大于false对应的包装类实例
  • String:按字符串中字符的Unicode值比较
  • Date、Time:后面的时间、日期比前面的日期大

所以TreeSet会自动地给集合中地大小进行排序,但前提是对象实现了Comparable接口,否则将不可行。还有一点需要说明,大部分类在实现compareTo(Object o)方法时,都需要将被比较对象强制类型转换成相同类型,否则无法比较。然而当添加一个对象到集合中去时,集合会调用对象compareTo()方法与集合中的其他元素进行比较,比较要是同一类的对象。

如果向TreeSet添加对象是自定义对象,则可以向TreeSet中添加多种类型对象,但这并不表明这是最好的选择,也不推荐这样干,当取出对象时,元素之间会报ClassCastException异常。

当把一个对象加入集合中时,集合调用对象的compareTo方法与容器的其他对象比较,然后根据红黑树结构找到它的储存位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到集合中。总之,如果要不报错,TreeSet中只能添加同一种类型的对象。

对于TreeSet集合而言,判断两个对象相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较相等,返回0,则相等。所以它的添加元素的规则和HashSet一样,假如通过equals方法比较相等,则它们记为同一对象,因此通过compareTo()方法返回0,只能存放一个对象,集合不会让第二个元素进去。同样,如果添加了可变对象,并且还修改了对象的实例变量,将导致集合中对象的顺序变化,然而TreeSet不会调整顺序(甚至会导致删除对象失败,TreeSet性能降低)。

2.定制排序

实现定制排序则可以通过Comparator接口,该接口内包含一个int compare(T o1,T o2)方法,用于比较o1、o2的大小,如果返回正整数,则o1>o2;若返回0,则o1=o2;若返回负整数,则o1<o2。但依然不可以添加不同类型的对象。

在实现定制排序之前,需要提供一个Comparator对象与TreeSet集合关联,由该对象对集合元素进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class M{
int age;
public M(int age){
this.age = age;
}
public String toString(){
return "M(age:" + age +" )";
}
}
public class Test{
public static void main(String[] args){//Lambda表达式
TreeSet t = new TreeSet((o1,o2)->{//o1,02这里没有具体类型
M m1 = (M)o1;
M m2 = (M)o2;
return m1.age>m2.age ? -1:m1.age<m2.age ? 1:0;
});
t.add(new M(5));
t.add(new M(-3));
t.add(new M(9));
System.out.println(t);
}//Lambda表达式实现了Comparator类型的对象
}//输出[M(age:9),M(age:5),M(age:-3)]降序排列

EnumSet类

专门为枚举类设计的集合类,EnumSet中所有元素必须是指定枚举类型的枚举值。值得一提的是EnumSet集合也是有序的,但它以枚举值在类中的定义顺序来决定集合元素的顺序。EnumSet在内部以位向量的形式储存元素,因此占用内存非常小,运行效率很好,特别是在批量操作时。

EnumSet不允许加入null元素,如果试图插入将会抛出NullPointerException异常。而如果只是判断集合中是否有null元素或删除它,将不会抛出异常,删除的话会返回false,因为没有null元素。

EnumSet集合没有给出任何构造器来创建对象,但还是提供了一些方法去创建的对象的。如下:

  • EnumSet allOf(Class elementType):创建一个包含指定枚举类所有枚举值的集合
  • EnumSet complementOf(EnumSet s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新集合包含原集合不包含的、此枚举类剩下的枚举值
  • EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet
  • EnumSet copyOf(EnumSet e):创建一个与指定EnumSet具有相同类型、相同元素的EnumSet集合
  • EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet
  • EnumSet range(E from ,E to):创建一个包含从from枚举值到to枚举值范围内枚举值的EnumSet集合
  • EnumSet of(E first…E rest ):创建一个包含一个或多个枚举值的EnumSet集合(传入的枚举值必须属于同一个枚举类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Colors{WHITE,YELLOW,RED,GREEN}
public class Test{
public static void main(String[] args){
EnumSet e1 = EnumSet.allOf(Colors.class);
EnumSet e2 = EnumSet.noneOf(Colors.class);//这里为[]
e2.add(Colors.WHITE);//这里为[WHITE]
EnumSet e3 = EnumSet.of(Colors.RED,Colors.GREEN);
System.out.println(e3);//输出[RED,GREEN]
EnumSet e4 = EnumSet.range(Colors.YELLOW,Colors.GREEN);
System.out.println(e4);
//[YELLOW,RED,GREEN]
EnumSet e5 = EnumSet.complementOf(e4);
//这里为[WHITE]
}
}

此外还可以复制另一个EnumSet集合中的元素来创建新集合,或者复制一个Collection集合元素来创建新的EnumSet集合。当复制Collection集合元素创建时,要求元素都来自同一个枚举类,否则抛出ClassCastException异常。

1
2
3
4
5
6
7
Collection c = new HashSet();
c.clear();
c.add(Colors.RED);
c.add(Colors.YELLOW);
EnumSet e = EnumSet.copyOf(c);
System.out.println(e);
//[RED,YELLOW]

各Set实现类的性能分析

HashSet总是比TreeSet的性能好,主要体现在添加、查询元素上,因为TreeSet采用了红黑树来维护集合的次序。而HashSet的子类LinkedHashSet对插入、查询操作比父类要稍慢些,这是由于采用了链表维护的缘故,造成了额外开销。但这并不影响LinkedHashSet发挥威力,然而这恰恰是它的优势,在遍历等批量操作上,LinkedHashSet更快。

EnumSet是所有Set类中性能最好的,缺点是只能保存一个枚举类的枚举值。总的说,Set的这三个实现类都是线程不安全的,即当有多个线程访问时,如果修改了Set集合,将不能同步,必须手动保证同步。方法是在创建Set集合时,通过Collections工具类的sychronizedXxx()方法来包装该集合(之后会提到)。

Java集合类-开篇

集合介绍Java集合类是一种特别的工具类,可以储存对象,并实现了常用的数据结构,另外还能保存具有映射关系的关联数组。集合大致分为Set,List,Queue,Map四种,其中Set代表无序、不可重复,List代表有序、重复,Map则代表具有映射关系,Java5增加了Queue集合,代表一种队列集合实现。Java集合就像一种容器

阅读全文

继承和组合、单例类及不可变类

继承和组合、单例类及不可变类继承 inheritance:继承是实现类复用的重要手段,所谓复用,就是可以多次使用,或者再次利用,不用继续重写成员变量和方法。但不代表没有缺点,最不好的地方:破坏封装。子类拓展父类时,若访问权限允许,则可直接访问父类的成员变量和方法,破坏了良好的封装性(Encapsulation) ,造成子类与父类的耦合。

阅读全文

String与BigDecimal基础类

String与BigDecimal

String, StringBuffer and StringBuilder

string有11种构造方法

1. 可变性

  • String不可变
  • StringBuffer 和 StringBuilder 可变

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

1
2
3
4
5
6
7
8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得同一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。

1
2
3
4
5
6
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

1
2
3
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

new String(“abc”)

使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。

  • “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
  • 而使用 new 的方式会在堆中创建一个字符串对象。

创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。

1
2
3
4
5
public class NewStringTest {
public static void main(String[] args) {
String s = new String("abc");
}
}

使用 javap -verbose 进行反编译,得到以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ...
Constant pool:
// ...
#2 = Class #18 // java/lang/String
#3 = String #19 // abc
// ...
#18 = Utf8 java/lang/String
#19 = Utf8 abc
// ...

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String abc
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
// ...

在 Constant Pool 中,#19 存储这字符串字面量 “abc”,#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。

以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。

1
2
3
4
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

BigDecimal

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。float和double只能用来做科学计算或者是工程计算,在商业计算中要用java.math.BigDecimal。BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。

BigDecimal一共有4个构造方法:

BigDecimal(int) 创建一个具有参数所指定整数值的对象。

BigDecimal(double) 创建一个具有参数所指定双精度值的对象。(不建议采用)

BigDecimal(long) 创建一个具有参数所指定长整数值的对象。

BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象

这里不建议采用第二种,原因:

1、参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入 newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1,但是它实际上等于0.1000000000000000055511151。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。

2、另一方面,String 构造方法是完全可预知的:写入 newBigDecimal(“0.1”) 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法 。

1
BigDecimal a = new BigDecimal("2.3");

当double必须用作BigDecimal的源时,请使用Double.toString(double)转成String,然后使用String构造方法,或使用BigDecimal的静态方法valueOf,如下:

1
BigDecimal a = new BigDecimal.valueOf(Double.toString(2.3));

对于常用的加,减,乘,除,BigDecimal类提供了相应的成员方法

1
2
3
4
public BigDecimal add(BigDecimal value); //加法
public BigDecimal subtract(BigDecimal value); //减法
public BigDecimal multiply(BigDecimal value); //乘法
public BigDecimal divide(BigDecimal value); //除法

总结:

  1. 在需要精确的小数计算时再使用BigDecimal,BigDecimal的性能比double和float差,在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。
  2. 尽量使用参数类型为String的构造函数。
  3. BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。

Java的异常、多态要点及抽象类和接口

一、java异常如下图,异常分为两类,错误和异常;异常分为运行异常和可查异常。这里主要掌握异常,因为平时会见的比较多。Exception类在java.lang包下,它是Throwable的子类,同样的Error类也是其子类。Error 用来指示运行时环境发生的错误,例如:JVM 内存溢出。一般地,程序不会从错误中恢复。异常类有两个主要的子类:IOException 类和 RuntimeException 类。

阅读全文