Java从零开始系列07:集合

学习目标

  • Java集合框架
  • 集合框架中的接口
  • 具体集合
  • 映射
  • 视图与包装器
  • 算法
  • 遗留的集合

一、Java集合框架

(一)集合接口与实现分离

Java集合类库将接口(interface)与实现(implementation)分离。下面以队列(queue)为例:队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数,并按照“先进先出”方式检索对象时应该使用队列。

队列接口的最简形式可能如下:

public interface Queue<E> // a simplified form of the interface in the standard library
{
	void add(E element);
	E remove();
	int size();
}

每一个实现都可以用一个实现了Queue接口的类表示:

public class CircularArrayQueue<E> implements Queue<E>  // not an actual library class
{
	private int head;
	private int tail;
	CircularArrayQueue(int capacity) {...}
	public void add(E element) {...}
	public E remove() {...}
	public int size() {...}
	public E[] elements;
}

public class LinkedListQueuee<E> implements Queue<E>  // not an actual library class
{
    private Link head;
    private Link tail;
    LinkedListQueuee() {...}
    public void add(E element) {...}
    public E remove() {...}
    public int size() {...}
}

当在程序中使用队列时,一旦已经构造了集合,就不需要知道究竟使用了哪种实现。因此,只有构造集合对象时,才会使用具体的类。可以使用接口类型存放集合引用:

Queue<Customer> expressLane = new CircularArrayQueue<>();
expressLane.add(new Customer("Harry"));

这样做也方便实现另一种不同的实现,只需要替换调用构造器的位置:

Queue<Customer> expressLane = new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));

(二)Collection接口

在Java类库中,集合的基本接口是Collection接口,这个接口有两个基本方法:

public interface Collection<E>
{
	boolean add(E element);
	Iterator<E> iterator();
	...
}

add方法用于向集合中添加元素,iterator方法用于返回一个实现了Iterator接口的对象,可以使用这个迭代器对象依次访问集合中的元素。

(三)迭代器

Iterator接口包含4个方法:

pubic interface Iterator<E>
{
	E next();
	boolean hasNext();
	void remove();
	default void forEachRemaining(Consumer<? super E> action);
}

每次调用next前调用hasNext方法。可以通过以下代码查看集合中的所有元素:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while(iter.hasNext();
{
	String element = iter.next();
	do something with element
}

也可以使用for each循环:

for (String element : c)
{
	do something with element
}

编译器简单地将“for each”循环转换为带有迭代器的循环。

也可以调用forEachRemaining方法并提供一个lambda表达式。
iterator.forEachRemaining(element -> do something with element);

remove方法用于删除上一次调用next方法时返回的元素。若要删除两个元素,则必须调用next遇过要删除的元素:

it.remove();
it.next();
it.remove();

二、集合框架中的接口

集合中有两个基本接口:Collection和Map。可以用以下方法在集合中插入元素:boolean add(E element)

不过,由于映射包含键/值对,所以要用put方法来插入:V put(K key, V value)

要从集合读取元素,可以用迭代器访问元素。不过,从映射中读取值则要使用get方法:V get(K key)

List是一个有序集合(ordered collection)。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后面这种方式称为随机访问(random access),可以按任意顺序访问元素。而使用迭代器时,必须顺序地访问元素。

List接口定义了多个用于随机访问的方法:

void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index, E element)

ListIterator接口是Iterator的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素:void add(E element)

Set接口等同于Collection接口,不过其方法的行为有更严谨的定义。集(set)的add方法不允许增加重复元素。要适当地定义集的equals方法:只要两个集包含相同的元素就认为它们是相等的,而不要求元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码。

SortedSet和SortedMap接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。

Java6还引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法。

三、具体集合

(一)链表

在Java程序设计语言中,所有链表实际上都是双向链接的(doubly linked)–即每个链接还存放着其前驱的引用。

在下面代码中,先添加3个元素,再将第2个元素删除:

var staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator<String> iter = staff.iterator();
String first = iter.next();	// visit first element
String Second = iter.next();	// visit second element
iter.remove();	// remove last visited element

链表是一个有序集合(ordered collection),LinkedList.add方法将对象添加到链表的尾部。对于需要添加到链表中间的元素,由于迭代器描述了集合中的位置,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。Iterator接口中没有add方法,实际上,集合类库提供了一个子接口ListIterator,其中包含add方法:

interface ListIterator<E> extends Iterator<E>
{
	void add(E element);
	...
}

这个方法不返回boolean类型的值,它假定add操作总会改变链表。

ListIterator接口有两个方法,可以反向遍历链表:

E previous();
boolean hasPrevious();

previous方法返回越过的对象。

LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象。

ListIterator<String> iter = staff.listIterator();

add方法在迭代器位置之前添加一个新对象。例如,越过链表中的第一个元素,在第二个元素前添加"Julet":

var staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Cral");
ListIterator<String> iter = staff.listIterator();
iter.next();	// skip past first element
iter.add("Juliet");

set方法用一个新元素替换调用next或previous方法返回的上一个元素。例如:

ListIterator<String> iter = list.listIterator();
String oldValue = iter.next();	// returns first element
iter.set(newValue);	// sets first element to newValue

链表迭代器设计为可以检测到集合的修改,如果集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出一个ConcurrentModificationException异常。

(二)数组列表

List接口用于描述一个有序集合,并且集合中每个元素的位置很重要。有两种访问元素协议:一种是通过迭代器,另一种是通过get和set方法随机地访问每个元素。后者不适用于链表。集合类库提供了一种大家熟悉的ArrayList类,这个类也实现了List接口。ArrayList封装了一个动态再分配的对象数组。

(三)散列集

散列表(hash table)可以快速地查找对象,它为每一个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例字段得出的一个整数。更准确地说,有不同数据的对象将产生不同的散列码。如String类的hashCode方法。

在Java中,散列表用链表数组实现。每个列表被称为桶(bucket),查找表中对象的位置时,要先计算其散列码,再与桶的总数取余,得到保存这个元素的桶的索引。填装时也会出现散列冲突(hash collision),需要与桶内其他元素比较看是否存在,不存在则通过一定规则进行填充。

为提高散列表的性能,可以设置一个初始的桶数,标准库使用的桶数是2的幂,默认值为16。

如果散列表太满,则需要再散列(rehashed),填装因子(load factor)可以确定何时对散列表进行再散列,一般认为0.75是合理的。

Java提供了一个HashSet类,实现了基于散列表的集,可以用add方法添加元素。

(四)树集

TreeSet类与散列集类型,树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,值将自动地按照排序后的顺序呈现。如:

var sorter = new TreeSet<String>();
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for (String s : sorter)
	System.out.println(s);

值将按照有序顺序打印:Amy Bob Carl,排序是用一个树数据结构完成的(当前实现使用红黑树(red-black tree))。每次将一个元素添加到树中时,都会将其放置在正确的排序位置上。

将一个元素添加到树中要比添加到散列表慢,但与检查数组或链表中的重复元素相比,使用树会快很多。查找新元素的正确位置平均需要 l o g 2 n log_2n log2n 次比较。

(五)队列与双端队列

双端队列(deque)允许在头部和尾部高效地添加或删除元素。不支持在队列中间添加或删除元素。Java6引入了Deque接口,ArrayDeque和LinkedList类实现了这个接口,都可以提供双端队列,可以根据需要扩展。

(六)优先队列

优先队列(priority queue)中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。并不需要对所有元素进行排序,优先队列使用了堆(heap),堆是一个可以自行组织的二叉树,其添加(add)和删除(remove)操作可以让最小的元素移动到根,而不必花费时间对元素进行排序。

与TreeSet一样,优先队列既可以保存实现了Comparable接口的类对象,也可以保存构造器中提供的Comparable对象。

优先队列的典型用法是任务调度。每一个任务有一个优先级,任务以随即顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除。

四、映射

映射(map)数据结构可以用来查找某些关键信息已知的关联的信息。映射用来存放键 / 值对。如果提供了键,就能查找到值。

(一)基本映射操作

Java为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。

散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。与键关联的值不进行散列或比较。

以下代码将建立一个散列映射来存储员工信息:

var staff = new HashMap<String, Employee>();	// HashMap implements Map
var harry = new Employee("Harry Hacker");
staff.put("987-98-9996", harry);

每当往映射中添加一个对象时,必须同时提供一个键。想要检索一个对象,必须使用键:

var id = "987-98-9996";
Employee e = staff.get(id);	// gets harry

如果映射中没有存储与给定键对应的信息,get返回null,也可以使用一个更好的默认值,然后使用getOrDefault方法:

Map<String, Integer> scores = ...;
int score = scores.getOrDefault(id, 0);	// gets 0 if the id is not present

键必须是唯一的。

要迭代处理映射的键和值,最容易的方法是使用forEach方法,可以提供一个接收键和值的lambda表达式:

scores.forEach(k, v) ->
	System.out.println("key=" + k + ", value=" + v));

(二)更新映射条目

正常情况下,可以得到一个键关联的原值,完成更新,再放回更新后的值。不过要考虑键第一次出现的特殊情况。例如,考虑使用映射统计一个单词在文件中出现的频度。看到一个单词时,将计数器增加1:
counts.put(word, currents.get(word) + 1);
第一次看到word时,get会返回null,出现一个NullPointrException异常。可以使用getOrDefault方法:
counts.put(word, counts.getOrDefault(word, 0) + 1);
另一种方法是首先调用putIfAbsent方法。只有当键原先存在时(或被映射到null)时才会放入一个值:
counts.putIfAbsent(word, 0); counts.put(word, counts.get(word) + 1);
merge方法可以简化这个操作。如果键原先不存在,下面调用:
counts.merge(word, 1, Integer::sum);
把word与1关联,否则使用 Integer::sum 函数组合原值和1(将原值与1求和)

(三)映射视图

集合框架不认为映射本身是一个集合。不过可以得到映射的视图(view)–这是实现了Collection接口或某个子接口的对象。

有3种视图:键集、值集合(不是一个集)以及键/值对集,键和键/值对可以构成一个集,因为映射中一个键只有一个副本。

Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();

会分别返回这3个视图。

可以枚举一个映射的所有键:

Set<String> keys = map.keySet();
for (String  key : keys)
{
	do something with key
}

如果想同时查看键和值,可以通过枚举映射条目来避免查找值:

for (Map.Entry<String, Empolyee> entry : staff.entrySet())
{
	String k = entry.getKey();
	Employee v = entry.getValue();
	do something with k, v
}
for (var entry : map.entrySet())
{
	do something with entry.getKey(), entry.getValue()
}

如今,只需要使用forEach方法:

map.forEach((k, v) -> {
	do something with k, v
}));

在键集视图上调用迭代器的remove方法,实际上会从映射中删除这个键和与它关联的值。但不能向键集视图中添加元素。

(四)链接散列集与映射

LinkedHashSetLinkedHashMap类会记住插入元素项的顺序,从而避免散列表中的项看起来顺序是随机的。在表中插入元素项时,就会并入到双向链表中。

链接散列表可以使用访问顺序而不是插入顺序来迭代处理映射条目。每次调用get或put时,受到影响的项会从当前位置删除,并放到项链表的尾部。可以调用:LinkedHashMap<K, V>(initialCapacity, loadFactor, true)

访问顺序对于实现缓存的“最近最少使用”原则十分重要。

(五)枚举集与映射

EnumSet是一个枚举类型元素集的高效实现,由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则对应的位被置为1.

EnumSet类没有公共的构造器。要使用静态工厂方法构造集:

enum Weekday ( MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);
EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);

可以使用Set接口的常用方法来修改EnumSet。

EnumMap是一个键类型为枚举类型的映射。可以直接且高效地实现为一个值数组。需要在构造器中指定键类型:
var personInCharge = new EnumMap<Weekday, Employee>(Weekday.class);

(六)标识散列映射

IdentityHashMap有特殊的用途。键的散列值不适用hashCode函数计算的而是System.identityHashCode方法。这是Object.hashCode根据对象的内存地址计算散列码时使用的方法。在两个对象进行比较时,IdentityHashMap类使用 == 而不是equals。

即不同的键对象即使内容相同,也被视为不同的对象。

五、视图与包装器

可以用视图(view)获得其他实现了Collection接口或Map接口的对象。keySet方法返回一个实现了Set接口的类对象,由这个类的方法操纵原映射。这种集合称为视图。

(一)小集合

Java 9引入了一些静态方法,可以生成给定元素的集或列表,以及给定键/值对的映射。例如:

List<String> names = List.of("Peter", "Paul", "Mary");
Set<Integer> numbers = Set.of(2, 3, 5);

会分别生成包含3个元素的一个列表和一个集。对于映射,需要指定键和值,如:

Map<String, Integer> scores = Map.of("Peter", 2, "Paul", 3, "Mary", 5);

List和Set接口有11个方法,分别有0到10个参数,另外还有一个参数个数可变的of方法。

对于Map接口,无法提供一个参数可变的版本,但它有一个静态方法ofEntries,可以接受任意多个Map.Entry<K, V>对象。如:

Map<String, Integer> scores = ofEntries(
	entry("Peter", 2),
	entry("Paul", 3),
	entry("Mary", 5));

of和ofEntries方法可以生成某些类的对象,这些类对于每个元素会有一个实例变量,或者有一个后备数组提供支持。

这些集合对象是不可修改的。否则会导致一个UnsupportedOperationException异常。

如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:

var names = new ArrayList<>(List.of("Peter", "Paul", "Mary"));

以下方法调用:

Collections.nCopies(n, anObject)

会返回一个实现了List接口的不可变对象。如创建一个100个字符串的List,每个串都设置为“DEFAULT”:

List<String> settings = Collections.nCopies(100, "DEFAULT");

这样存储开销很小,对象只存储一次。

(二)子范围

可以为很多集合建立子范围(subrange)视图。如,有一个列表staff,想取出第10~19个元素,可以使用subList方法来获取这个列表子范围视图:
List<Employee> group2 = staff.subList(10,20);
第一个索引包含在内,第二个索引不包含在内。

可以对子范围应用任何操作,而且操作会自动反映到整个列表。如,可以删除整个子范围:
group2.clear(); // staff reduction
元素会自动从staff列表中清楚,且group2为空。

对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围。SortedSet接口声明了3个方法:

SortedSet<E> subSet(E from, E to);
SortedSet<E> headSet(E to);
SortedSet<E> tailSet(E from);

这些方法将返回大于等于from且小于to的所有元素构成的子集。有序映射也有类似的方法:

SortedSet<K, V> subMap(K from, K to);
SortedSet<K, V> headMap(K to);
SortedSet<K, V> tailMap(K from);

这些方法会返回映射视图,该映射包含键落在指定范围内的所有元素。

Java6引入了NavigableSet接口允许更多地控制这些子范围操作。可以指定是否包括边界。

NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
NavigableSet<E> tailSet(E from, boolean fromInclusive)

(三)不可修改的视图

Collections类还有几个方法,可以生成集合的不可修改视图(unmodifiable view)。视图对现有集合增加一个运行时的检查如果发现试图对集合进行修改,就会抛出一个异常,集合仍保持不变。如:

var staff = new LinkedList<String>();
...
lookAt(Collections.unmodifiableList(staff));

不可修改视图不是集合本身不可更改,仍然可以通过集合的原始引用对集合进行修改,且仍然可以对集合的元素调用更改器方法。

由于视图只是包装了接口而不是具体的集合对象,所以只能访问接口中定义的方法。

(四)同步视图

如果从多个线程访问集合,就必须确保集合不会被意外地破坏。

类库的设计者使用视图机制来确保常规集合是线程安全的,而没有实现线程安全的集合类。如,Collections类的静态synchronizeMap方法可以将任何一个映射转换成有同步访问方法的Map:
var map = Collections.synchronizedMap(new HashMap<String, Employee>());

(五)检查型视图

“检查型”视图用来对泛型类型可能出现的问题提供调试支持。下面定义了一个安全列表:
List<String> safeStrings = Collections.checkedList(strings, String.class);
这个视图的add方法将检查插入的对象是否属于给定类。

六、算法

泛型集合接口有一个很大的优点,即算法只需要实现一次。

(一)排序与混排

Collections类中的sort方法可以对实现了List接口的集合进行排序:

var staff = new LinkedList<String>();
fill collection
Collections.sort(staff)

这个方法假定列表元素实现了Comparable接口。

也可以使用其他方式对列表排序,可以使用List接口的sort方法并传入一个Comparator对象。如按工资对一个员工列表排序:

staff.sort(Comparator.comparingDouble(Employee::getSalary);

如果想按照降序对列表进行排序,可以使用静态的遍历方法Collections.reverseOrder(),这个方法将返回一个比较器,比较器则返回b.compareTo(a)。如:

staff.sort(Comparater.reverseOrder())

这个方法根据元素类型的compareTo方法所给定的排序顺序,按逆序对列表排序。同样地:

staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())

将按工资逆序排序。

下面是有关术语的定义:

  • 如果列表支持set方法,则是可修改的(modifiable)
  • 如果列表支持add和remove方法,则是可改变大小的(resizable)

Collections类有一个算法shuffle,会随机地混排列表中元素的顺序。如:

ArrayList<Card> cards = ...
Collections.shuffle(staff);

(二)二分查找

Collections类地binarySearch方法实现了二分查找算法。注意:集合必须是有序的。如果集合没有采用Comparable接口的compareTo方法进行排序,还需要提供一个比较器对象:

i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);

binarySearch方法返回一个非负值,表示匹配对象地索引。返回负值表示没有匹配的元素。可以利用返回值来计算应将element插入到集合的哪个位置,位置为:insertionPoint = -i - 1;。这并不是简单的 -i ,因为0值是不确定的,即:

if (i < 0)
	c.add(-i - 1, element);

将把元素插入到正确的位置上。

只有采用随机访问,二分查找才有意义。

(三)批操作

很多操作会“成批”复制或删除元素。如:
coll1.removeAll(coll2);将从coll1中删除coll2中出现的所有元素。
coll1.retainAll(coll2);将从coll1中删除所有未在coll2中出现的元素。

例如,假设希望找出两个集的交集(intersection),可以先建立一个新集来存放结果:

var result = new HashSet<String>(firstSet);

每一个集合都会有这样一个构造器,其参数是包含初始值的另一个集合。

再使用retainAll方法:

result.retainAll(secondSet);

这会保留两个集中都出现的元素。

也可以对视图应用一个批操作。如一个映射,将ID映射到员工对象,另有一个不在聘用的所有员工的ID集:

Map<String, Employee> staffMap = ...
Set<String> terminatedIDs = ...

只需要建立一个键集,并删除终止聘用关系的所有员工ID:

staffMap.keySet().removeAll(terminatedIDs);

通过使用子范围视图,可以把批操作限制在子列表和子集上。如希望把一个列表的前10个元素增加到另一个容器,可以建立一个子列表选出前10个元素:

relocated.addAll(staff.subList(0,10));

这个子范围还可以完成更改操作。

staff.subList(0, 10).clear();

(四)集合与数组的转换

Java平台API大部分内容是在集合框架创建之前设计的,有时候需要在传统的数组和更现代的集合之间进行转换。List.of可以把一个数组转换为集合:

Sting[] values = ...
var staff = new HashSet<>(List.of(values));

从集合得到数组会更困难些,可以使用toArray方法:

Object[] values = staff.toArray();

这样会得到一个对象数组,不能对其使用强制类型转换。
String[] values = (String[]) staff.toArray(); // error
toArray方法返回的数组创建为Object[] 数组,不能改变其类型。实际上,必须使用toArray方法的一个变体,提供一个指定类型而且长度为0的数组。这样一来,返回的数组就会创建为相同的数组类型:

String[] values = staff.toArray(new String[0]);

也可以构造一个大小正确的数组:

staff.toArray(new String[staff.size()]);

这种情况下,不会创建新数组。

(五)编写自己的算法

编写自己的算法时,应尽可能地使用接口,而不要使用具体的实现。

七、遗留的集合

(一)Hashtable类

经典的Hashtable类与HashMap类的作用一样,接口也基本相同。与Vector类的方法一样,Hashtable方法也是同步的。如果对与遗留代码的兼容性没有任何要求,就应该使用HashMap。如果需要并发访问,则要使用ConcurrentHashMap。

(二)枚举

遗留的集合使用Enumeration接口遍历元素序列。该接口有两个方法,即hasMoreElements和nextElement。这两个方法类似于Iterator接口的hasNext方法和next方法。

如果发现遗留的类实现了这个接口,可以使用Collections.list将元素收集到一个ArrayList中。例如,LogManager类只是将登录者的名字提供为一个Enumeration。可以如下得到所有登陆者的名字:

ArrayList<String> loggerNames = Collections.list(LogManager.getLoggerNames());

或者在Java9中,可以把一个枚举转换为一个迭代器:

Logmanager.getLoggerNames().asIterator().forEachRemaining(n -> { ... });

有时还会遇到遗留的方法希望得到枚举参数。静态方法Collections.enumeration将产生一个枚举对象,枚举集合中的元素。如:

List<InputStream> streams = ...
var in = new SequenceInputStream(Collections.enumeration(streams);
	// the SequenceInputStream constructor expects an enumeration

(三)属性映射

属性映射(property map)是一个特殊类型的映射结构。它下面有3个特性:

  • 键与值都是字符串
  • 这个映射可以很容易地保存到文件以及从文件加载
  • 有一个二级表存放默认值

实现属性映射的Java平台类名为Properties。属性映射对于指定程序的配置选项很有用。例如:

var settings = new Properties();
settings.setProperty("width", "600.0");
settings.setProperty("filename", "/home/cay/raven.html");

(四)栈

标准库中包含Stack类,包含push方法和pop方法,同时扩展了Vector类。

(五)位集

Java平台的BitSet类用于存储一个位序列。如果需要高效地存储位序列,就可以使用位集。位集将位包装在字节中。

BitSet类提供了一个便于读取、设置或重置各个位的接口。使用这个接口可以避免掩码和其他调整位的操作。如,对一个名为bucketOfBits的BitSet,bucketOfBits.get(i),如果第i位处于“开”状态,就返回true;否则返回false。类似的,bucketOfBits.set(i)将第i位置为“开”状态。最后,backetOfBits.clear(i)将第i位置为“关”状态。


参考资料:

狂神说Java
Java核心技术 卷I(第11版)


上一章:Java从零开始系列06:泛型程序设计
下一章:Java从零开始系列08:图形用户界面程序设计

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习Java需要有一定的学习计划和方法。首先,可以从学习Java的基础知识开始,如变量、数据类型、运算符、流程控制语句和面向对象等。可以选择一本适合初学者的Java教材,系统地学习基础知识,并进行练习和实践,加深对概念和语法的理解。 其次,要掌握Java开发环境和工具,如安装JDK(Java Development Kit)、配置开发环境和集成开发工具(IDE)等。学习如何使用IDE编写Java程序、调试和运行程序,掌握基本的开发流程和调试技巧。 第三,了解并学习Java的常用类和库,如字符串处理、文件操作、集合框架等。可以通过查阅官方文档或相关书籍,了解这些类和库的用法和功能,并通过实例进行实践和练习。 第四,参与Java编程实践和项目开发。可以尝试完成一些简单的Java编程任务,如编写一个简单的控制台程序、实现一个小游戏等。通过实践,锻炼自己的编程能力和解决问题的能力,并逐渐提高编程水平。 此外,要善于查阅资料和参考优秀的Java代码。在学习过程中,难免会遇到一些问题,可以通过查阅资料和参考他人的代码来解决问题。同时,也可以参与相关的Java技术社区和论坛,与其他Java开发者交流学习经验和解决问题。 总之,从零开始学习Java需要坚持、耐心和实践。通过系统学习基础知识、掌握开发环境和工具、了解常用类和库,并进行实践和项目开发,逐渐提高自己的编程水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值