计算机程序的思维逻辑 (93) - 函数式数据处理 (下)

上节初步介绍了Java 8中的函数式数据处理,对于collect方法,我们只是演示了其最基本的应用,它还有很多强大的功能,比如,可以分组统计汇总,实现类似数据库查询语言SQL中的group by功能。

具体都有哪些功能?有什么用?如何使用?基本原理是什么?本节进行详细讨论,我们先来进一步理解下collect方法。

理解collect

上节中,过滤得到90分以上的学生列表,代码是这样的:

List<Student> above90List = students.stream()
        .filter(t->t.getScore()>90)
        .collect(Collectors.toList());

最后的collect调用看上去很神奇,它到底是怎么把Stream转换为List<Student>的呢?先看下collect方法的定义:

<R, A> R collect(Collector<? super T, A, R> collector)

它接受一个收集器collector作为参数,类型是Collector,这是一个接口,它的定义基本是:

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    Set<Characteristics> characteristics();
}

在顺序流中,collect方法与这些接口方法的交互大概是这样的:

//首先调用工厂方法supplier创建一个存放处理状态的容器container,类型为A
A container = collector.supplier().get();

//然后对流中的每一个元素t,调用累加器accumulator,参数为累计状态container和当前元素t
for (T t : data)
   collector.accumulator().accept(container, t);

//最后调用finisher对累计状态container进行可能的调整,类型转换(A转换为R),并返回结果
return collector.finisher().apply(container);

combiner只在并行流中有用,用于合并部分结果。characteristics用于标示收集器的特征,Collector接口的调用者可以利用这些特征进行一些优化,Characteristics是一个枚举,有三个值:CONCURRENT, UNORDERED和IDENTITY_FINISH,它们的含义我们后面通过例子简要说明,目前可以忽略。

Collectors.toList()具体是什么呢?看下代码:

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

它的实现类是CollectorImpl,这是Collectors内部的一个私有类,实现很简单,主要就是定义了两个构造方法,接受函数式参数并赋值给内部变量。对toList来说:

  • supplier的实现是ArrayList::new,也就是创建一个ArrayList作为容器
  • accumulator的实现是List::add,也就是将碰到的每一个元素加到列表中,
  • 第三个参数是combiner,表示合并结果
  • 第四个参数CH_ID是一个静态变量,只有一个特征IDENTITY_FINISH,表示finisher没有什么事情可以做,就是把累计状态container直接返回

也就是说,collect(Collectors.toList())背后的伪代码如下所示:

List<T> container = new ArrayList<>();
for (T t : data)
   container.add(t);
return container;

与toList类似的容器收集器还有toSet, toCollection, toMap等,我们来看下。

容器收集器

toSet

toSet的使用与toList类似,只是它可以排重,就不举例了。toList背后的容器是ArrayList,toSet背后的容器是HashSet,其代码为:

public static <T>
Collector<T, ?, Set<T>> toSet() {
    return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_UNORDERED_ID);
}

CH_UNORDERED_ID是一个静态变量,它的特征有两个,一个是IDENTITY_FINISH,表示返回结果即为Supplier创建的HashSet,另一个是UNORDERED,表示收集器不会保留顺序,这也容易理解,因为背后容器是HashSet。

toCollection

toCollection是一个通用的容器收集器,可以用于任何Collection接口的实现类,它接受一个工厂方法Supplier作为参数,具体代码为:

public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
    return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                               (r1, r2) -> { r1.addAll(r2); return r1; },
                               CH_ID);
}

比如,如果希望排重但又希望保留出现的顺序,可以使用LinkedHashSet,Collector可以这么创建:

Collectors.toCollection(LinkedHashSet::new)

toMap

toMap将元素流转换为一个Map,我们知道,Map有键和值两部分,toMap至少需要两个函数参数,一个将元素转换为键,另一个将元素转换为值,其基本定义为:

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
    Function<? super T, ? extends K> keyMapper,
    Function<? super T, ? extends U> valueMapper)

返回结果为Map<K,U>,keyMapper将元素转换为键,valueMapper将元素转换为值。比如,将学生流转换为学生名称和分数的Map,代码可以为:

Map<String,Double> nameScoreMap = students.stream().collect(
        Collectors.toMap(Student::getName, Student::getScore));

这里,Student::getName是keyMapper,Student::getScore是valueMapper。

实践中,经常需要将一个对象列表按主键转换为一个Map,以便以后按照主键进行快速查找,比如,假定Student的主键是id,希望转换学生流为学生id和学生对象的Map,代码可以为:

Map<String, Student> byIdMap = students.stream().collect(
        Collectors.toMap(Student::getId, t -> t));

t->t是valueMapper,表示值就是元素本身,这个函数用的比较多,接口Function定义了一个静态函数identity表示它,也就是说,上面的代码可以替换为:

Map<String, Student> byIdMap = students.stream().collect(
        Collectors.toMap(Student::getId, Function.identity()));

上面的toMap假定元素的键不能重复,如果有重复的,会抛出异常,比如:

Map<String,Integer> strLenMap = Stream.of("abc","hello","abc").collect(
        Collectors.toMap(Function.identity(), t->t.length()));

希望得到字符串与其长度的Map,但由于包含重复字符串"abc",程序会抛出异常。这种情况下,我们希望的是程序忽略后面重复出现的元素,这时,可以使用另一个toMap函数:

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
    Function<? super T, ? extends K> keyMapper,
    Function<? super T, ? extends U> valueMapper,
    BinaryOperator<U> mergeFunction)    

相比前面的toMap,它接受一个额外的参数mergeFunction,它用于处理冲突,在收集一个新元素时,如果新元素的键已经存在了,系统会将新元素的值与键对应的旧值一起传递给mergeFunction得到一个值,然后用这个值给键赋值。

对于前面字符串长度的例子,新值与旧值其实是一样的,我们可以用任意一个值,代码可以为:

Map<String,Integer> strLenMap = Stream.of("abc","hello","abc").collect(
        Collectors.toMap(Function.identity(),
                t->t.length(), (oldValue,value)->value));

有时,我们可能希望合并新值与旧值,比如一个联系人列表,对于相同的联系人,我们希望合并电话号码,mergeFunction可以定义为:

BinaryOperator<String> mergeFunction = (oldPhone,phone)->oldPhone+","+phone;

toMap还有一个更为通用的形式:

public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(
    Function<? super T, ? extends K> keyMapper,
    Function<? super T, ? extends U> valueMapper,
    BinaryOperator<U> mergeFunction,
    Supplier<M> mapSupplier) 

相比前面的toMap,多了一个mapSupplier,它是Map的工厂方法,对于前面两个toMap,其mapSupplier其实是HashMap::new。我们知道,HashMap是没有任何顺序的,如果希望保持元素出现的顺序,可以替换为LinkedHashMap,如果希望收集的结果排序,可以使用TreeMap

toMap主要用于顺序流,对于并发流,Collectors有专门的名称为toConcurrentMap的收集器,它内部使用ConcurrentHashMap,用法类似,具体我们就不讨论了。

字符串收集器

除了将元素流收集到容器中,另一个常见的操作是收集为一个字符串。比如,获取所有的学生名称,用逗号连接起来,传统上,代码看上去像这样:

StringBuilder sb = new StringBuilder();
for(Student t : students){
    if(sb.length()>0){
        sb.append(",");
    }
    sb.append(t.getName());
}
return sb.toString();

针对这种常见的需求,Collectors提供了joining收集器:

public static Collector<CharSequence, ?, String> joining()
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
public static Collector<CharSequence, ?, String> joining(
    CharSequence delimiter, CharSequence prefix, CharSequence suffix) 

第一个就是简单的把元素连接起来,第二个支持一个分隔符,第三个更为通用,可以给整个结果字符串加个前缀和后缀。比如:

String result = Stream.of("abc","老马","hello")
        .collect(Collectors.joining(",", "[", "]"));
System.out.println(result);                                                  

输出为:

[abc,老马,hello]

joining的内部也利用了StringBuilder,比如,第一个joining函数的代码为:

public static Collector<CharSequence, ?, String> joining() {
    return new CollectorImpl<CharSequence, StringBuilder, String>(
            StringBuilder::new, StringBuilder::append,
            (r1, r2) -> { r1.append(r2); return r1; },
            StringBuilder::toString, CH_NOID);
}

supplier是StringBuilder::new,accumulator是StringBuilder::append,finisher是StringBuilder::toString,CH_NOID表示特征集为空。

分组

分组类似于数据库查询语言SQL中的group by语句,它将元素流中的每个元素分到一个组,可以针对分组再进行处理和收集,分组的功能比较强大,我们逐步来说明。

为便于举例,我们先修改下学生类Student,增加一个字段grade,表示年级,改下构造方法:

public Student(String name, String grade, double score) {
    this.name = name;
    this.grade = grade;
    this.score = score;
}

示例学生列表students改为:

static List<Student> students = Arrays.asList(new Student[] {
        new Student("zhangsan", "1", 91d),
        new Student("lisi", "2", 89d),
        new Student("wangwu", "1", 50d),
        new Student("zhaoliu", "2", 78d),
        new Student("sunqi", "1", 59d)});            

基本用法

最基本的分组收集器为:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier)

参数是一个类型为Function的分组器classifier,它将类型为T的元素转换为类型为K的一个值,这个值表示分组值,所有分组值一样的元素会被归为同一个组,放到一个列表中,所以返回值类型是Map<K, List<T>>。 比如,将学生流按照年级进行分组,代码为:

Map<String, List<Student>> groups = students.stream()
        .collect(Collectors.groupingBy(Student::getGrade));

学生会分为两组,第一组键为"1",分组学生包括"zhangsan", "wangwu"和"sunqi",第二组键为"2",分组学生包括"lisi", "zhaoliu"。

这段代码基本等同于如下代码:

Map<String, List<Student>> groups = new HashMap<>();
for (Student t : students) {
    String key = t.getGrade();
    List<Student> container = groups.get(key);
    if (container == null) {
        container = new ArrayList<>();
        groups.put(key, container);
    }
    container.add(t);
}
System.out.println(groups);

显然,使用groupingBy要简洁清晰的多,但它到底是怎么实现的呢?

基本原理

groupingBy的代码为:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
}

它调用了第二个groupingBy方法,传递了toList收集器,其代码为:

public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                      Collector<? super T, A, D> downstream) {
    return groupingBy(classifier, HashMap::new, downstream);
}

这个方法接受一个下游收集器downstream作为参数,然后传递给下面更通用的函数:

public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                              Supplier<M> mapFactory,
                              Collector<? super T, A, D> downstream)

classifier还是分组器,mapFactory是返回Map的工厂方法,默认是HashMap::new,downstream表示下游收集器,下游收集器负责收集同一个分组内元素的结果。

对最通用的groupingBy函数返回的收集器,其收集元素的基本过程和伪代码为:

//先创建一个存放结果的Map
Map map = mapFactory.get();
for (T t : data) {
    // 对每一个元素,先分组
    K key = classifier.apply(t);
    // 找存放分组结果的容器,如果没有,让下游收集器创建,并放到Map中
    A container = map.get(key);
    if (container == null) {
        container = downstream.supplier().get();
        map.put(key, container);
    }
    // 将元素交给下游收集器(即分组收集器)收集
    downstream.accumulator().accept(container, t);
}
// 调用分组收集器的finisher方法,转换结果
for (Map.Entry entry : map.entrySet()) {
    entry.setValue(downstream.finisher().apply(entry.getValue()));
}
return map;

在最基本的groupingBy函数中,下游收集器是toList,但下游收集器还可以是其他收集器,甚至是groupingBy,以构成多级分组,下面我们来看更多的示例。

分组计数、找最大/最小元素

将元素按一定标准分为多组,然后计算每组的个数,按一定标准找最大或最小元素,这是一个常见的需求,Collectors提供了一些对应的收集器,一般用作下游收集器,比如:

//计数
public static <T> Collector<T, ?, Long> counting()
//计算最大值
public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator)
//计算最小值
public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator)

还有更为通用的名为reducing的归约收集器,我们就不介绍了,下面,看一些例子。

为了便于使用Collectors中的方法,我们将其中的方法静态导入,即加入如下代码:

import static java.util.stream.Collectors.*;

统计每个年级的学生个数,代码可以为:

Map<String, Long> gradeCountMap = students.stream().collect(
        groupingBy(Student::getGrade, counting()));

统计一个单词流中每个单词的个数,按出现顺序排序,代码示例为:

Map<String, Long> wordCountMap =
        Stream.of("hello","world","abc","hello").collect(
            groupingBy(Function.identity(), LinkedHashMap::new, counting()));

获取每个年级分数最高的一个学生,代码可以为:

Map<String, Optional<Student>> topStudentMap = students.stream().collect(
        groupingBy(Student::getGrade,
                maxBy(Comparator.comparing(Student::getScore))));

需要说明的是,这个分组收集结果是Optional<Student>,而不是Student,这是因为maxBy处理的流可能是空流,但对我们的例子,这是不可能的,为了直接得到Student,可以使用Collectors的另一个收集器collectingAndThen,在得到Optional<Student>后调用Optional的get方法,如下所示:

Map<String, Student> topStudentMap = students.stream().collect(
        groupingBy(Student::getGrade,
                collectingAndThen(
                        maxBy(Comparator.comparing(Student::getScore)),
                        Optional::get)));

关于collectingAndThen,我们待会再进一步讨论。

分组数值统计

除了基本的分组计数,还经常需要进行一些分组数值统计,比如求学生分数的和、平均分、最高分/最低分等,针对int,long和double类型,Collectors提供了专门的收集器,比如:

//求平均值,int和long也有类似方法
public static <T> Collector<T, ?, Double>
    averagingDouble(ToDoubleFunction<? super T> mapper)
//求和,long和double也有类似方法
public static <T> Collector<T, ?, Integer>
    summingInt(ToIntFunction<? super T> mapper)
//求多种汇总信息,int和double也有类似方法
//LongSummaryStatistics包括个数、最大值、最小值、和、平均值等多种信息
public static <T> Collector<T, ?, LongSummaryStatistics>
    summarizingLong(ToLongFunction<? super T> mapper)

比如,按年级统计学生分数信息,代码可以为:

Map<String, DoubleSummaryStatistics> gradeScoreStat =
    students.stream().collect(
            groupingBy(Student::getGrade,
                    summarizingDouble(Student::getScore)));

分组内的map

对于每个分组内的元素,我们感兴趣的可能不是元素本身,而是它的某部分信息,在上节介绍的Stream API中,Stream有map方法,可以将元素进行转换,Collectors也为分组元素提供了函数mapping,如下所示:

public static <T, U, A, R>
Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
    Collector<? super U, A, R> downstream)

交给下游收集器downstream的不再是元素本身,而是应用转换函数mapper之后的结果。比如,对学生按年级分组,得到学生名称列表,代码可以为:

Map<String, List<String>> gradeNameMap =
        students.stream().collect(
                groupingBy(Student::getGrade,
                        mapping(Student::getName, toList())));
System.out.println(gradeNameMap);      

输出为:

{1=[zhangsan, wangwu, sunqi], 2=[lisi, zhaoliu]}

分组结果处理(filter/sort/skip/limit)

对分组后的元素,我们可以计数,找最大/最小元素,计算一些数值特征,还可以转换后(map)再收集,那可不可以像上节介绍的Stream API一样,进行排序(sort)、过滤(filter)、限制返回元素(skip/limit)呢?Collector没有专门的收集器,但有一个通用的方法:

public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(
    Collector<T,A,R> downstream, Function<R,RR> finisher)

这个方法接受一个下游收集器downstream和一个finisher,返回一个收集器,它的主要代码为:

return new CollectorImpl<>(downstream.supplier(),
    downstream.accumulator(),
    downstream.combiner(),
    downstream.finisher().andThen(finisher),
    characteristics);

也就是说,它在下游收集器的结果上又调用了finisher。利用这个finisher,我们可以实现多种功能,下面看一些例子。

收集完再排序,可以定义如下方法:

public static <T> Collector<T, ?, List<T>> collectingAndSort(
        Collector<T, ?, List<T>> downstream,
        Comparator<? super T> comparator) {
    return Collectors.collectingAndThen(downstream, (r) -> {
        r.sort(comparator);
        return r;
    });
}

比如,将学生按年级分组,分组内学生按照分数由高到低进行排序,利用这个方法,代码可以为:

Map<String, List<Student>> gradeStudentMap =
    students.stream().collect(
            groupingBy(Student::getGrade,
                    collectingAndSort(toList(),
                            Comparator.comparing(Student::getScore).reversed())));

针对这个需求,也可以先对流进行排序,然后再分组。

收集完再过滤,可以定义如下方法:

public static <T> Collector<T, ?, List<T>> collectingAndFilter(
        Collector<T, ?, List<T>> downstream,
        Predicate<T> predicate) {
    return Collectors.collectingAndThen(downstream, (r) -> {
        return r.stream().filter(predicate).collect(Collectors.toList());
    });
}

比如,将学生按年级分组,分组后,每个分组只保留不及格的学生(低于60分),利用这个方法,代码可以为:

Map<String, List<Student>> gradeStudentMap =
    students.stream().collect(
            groupingBy(Student::getGrade,
                    collectingAndFilter(toList(), t->t.getScore()<60)));

针对这个需求,也可以先对流进行过滤,然后再分组。

收集完,只返回特定区间的结果,可以定义如下方法:

public static <T> Collector<T, ?, List<T>> collectingAndSkipLimit(
        Collector<T, ?, List<T>> downstream, long skip, long limit) {
    return Collectors.collectingAndThen(downstream, (r) -> {
        return r.stream().skip(skip).limit(limit).collect(Collectors.toList());
    });
}

比如,将学生按年级分组,分组后,每个分组只保留前两名的学生,代码可以为:

Map<String, List<Student>> gradeStudentMap =
    students.stream()
        .sorted(Comparator.comparing(Student::getScore).reversed())
        .collect(groupingBy(Student::getGrade,
                    collectingAndSkipLimit(toList(), 0, 2)));

这次,我们先对学生流进行了排序,然后再进行了分组。

分区

分组的一个特殊情况是分区,就是将流按true/false分为两个组,Collectors有专门的分区函数:

public static <T> Collector<T, ?, Map<Boolean, List<T>>>
    partitioningBy(Predicate<? super T> predicate)
public static <T, D, A> Collector<T, ?, Map<Boolean, D>>
    partitioningBy(Predicate<? super T> predicate,
    Collector<? super T, A, D> downstream)    

第一个的下游收集器为toList(),第二个可以指定一个下游收集器。

比如,将学生按照是否及格(大于等于60分)分为两组,代码可以为:

Map<Boolean, List<Student>> byPass = students.stream().collect(
    partitioningBy(t->t.getScore()>=60));

按是否及格分组后,计算每个分组的平均分,代码可以为:

Map<Boolean, Double> avgScoreMap = students.stream().collect(
        partitioningBy(t->t.getScore()>=60,
            averagingDouble(Student::getScore)));    

多级分组

groupingBy和partitioningBy都可以接受一个下游收集器,而下游收集器又可以是分组或分区。

比如,按年级对学生分组,分组后,再按照是否及格对学生进行分区,代码可以为:

Map<String, Map<Boolean, List<Student>>> multiGroup =
        students.stream().collect(
                groupingBy(Student::getGrade,
                        partitioningBy(t->t.getScore()>=60)));    

小结

本节主要讨论了各种收集器,包括容器收集器、字符串收集器、分组和分区收集器等。

对于分组和分区,它们接受一个下游收集器,对同一个分组或分区内的元素进行进一步收集,下游收集器还可以是分组或分区,以构建多级分组,有一些收集器主要用于分组,比如counting, maxBy, minBy,  summarizingDouble等。

mapping和collectingAndThen也都接受一个下游收集器,mapping在把元素交给下游收集器之前先进行转换,而collectingAndThen对下游收集器的结果进行转换,组合利用它们,可以构造更为灵活强大的收集器。

至此,关于Java 8中的函数式数据处理Stream API,我们就介绍完了,Stream API提供了集合数据处理的常用函数,利用它们,可以简洁地实现大部分常见需求,大大减少代码,提高可读性。

对于并发编程,Java 8也提供了一个新的类CompletableFuture,类似于Stream API对集合数据的流水线式操作,使用CompletableFuture,可以实现对多个异步任务进行流水线式操作,它具体是什么呢?

(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.java8.c93下)

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

时间: 2024-08-26 13:53:54

计算机程序的思维逻辑 (93) - 函数式数据处理 (下)的相关文章

计算机程序的思维逻辑 (28) - 剖析包装类 (下)

本节探讨Character类,它的基本用法我们在包装类第一节已经介绍了,本节不再赘述.Character类除了封装了一个char外,还有什么可介绍的呢?它有很多静态方法,封装了Unicode字符级别的各种操作,是Java文本处理的基础,注意不是char级别,Unicode字符并不等同于char,本节详细介绍这些方法以及相关的Unicode知识. 在介绍这些方法之前,我们需要回顾一下字符在Java中的表示方法,我们在第六节.第七节.第八节介绍过编码.Unicode.char等知识,我们先简要回顾一

计算机程序的思维逻辑 (29) - 剖析String

上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比较简单直接的,我们来看下. 基本用法 可以通过常量定义String变量 String name = "老马说编程"; 也可以通过new创建String String name = new String("老马说编程"); String可以直接使用+和+=运算符,如: S

计算机程序的思维逻辑 (25) - 异常 (下)

上节我们介绍了异常的基本概念和异常类,本节我们进一步介绍对异常的处理,我们先来看Java语言对异常处理的支持,然后探讨在实际中到底应该如何处理异常. 异常处理 catch匹配 上节简单介绍了使用try/catch捕获异常,其中catch只有一条,其实,catch还可以有多条,每条对应一个异常类型,比如说: try{ //可能触发异常的代码 }catch(NumberFormatException e){ System.out.println("not valid number"); }

计算机程序的思维逻辑 (22) - 代码的组织机制

使用任何语言进行编程都有一个类似的问题,那就是如何组织代码,具体来说,如何避免命名冲突?如何合理组织各种源文件?如何使用第三方库?各种代码和依赖库如何编译连接为一个完整的程序? 本节就来讨论Java中的解决机制,具体包括包.jar包.程序的编译与连接,从包开始. 包的概念 使用任何语言进行编程都有一个相同的问题,就是命名冲突,程序一般不全是一个人写的,会调用系统提供的代码.第三方库中的代码.项目中其他人写的代码等,不同的人就不同的目的可能定义同样的类名/接口名,Java中解决这个问题的方法就是包

计算机程序的思维逻辑 (23) - 枚举的本质

前面系列,我们介绍了Java中表示和操作数据的基本数据类型.类和接口,本节探讨Java中的枚举类型. 所谓枚举,是一种特殊的数据,它的取值是有限的,可以枚举出来的,比如说一年就是有四季.一周有七天,虽然使用类也可以处理这种数据,但枚举类型更为简洁.安全和方便. 下面我们就来介绍枚举的使用,同时介绍其实现原理. 基础 基本用法 定义和使用基本的枚举是比较简单的,我们来看个例子,为表示衣服的尺寸,我们定义一个枚举类型Size,包括三个尺寸,小/中/大,代码如下: public enum Size {

计算机程序的思维逻辑 (21) - 内部类的本质

内部类 之前我们所说的类都对应于一个独立的Java源文件,但一个类还可以放在另一个类的内部,称之为内部类,相对而言,包含它的类称之为外部类. 为什么要放到别的类内部呢?一般而言,内部类与包含它的外部类有比较密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上也往往更为简洁. 不过,内部类只是Java编译器的概念,对于Java虚拟机而言,它是不知道内部类这回事的, 每个内部类最后都会被编译为一个独立的类,生成一个独立的字节码文件. 也就是说,每个内部

计算机程序的思维逻辑 (90) - 正则表达式 (下 - 剖析常见表达式)

?88节介绍了正则表达式的语法,上节介绍了正则表达式相关的Java API,本节来讨论和分析一些常用的正则表达式,具体包括: 邮编 电话号码,包括手机号码和固定电话号码 日期和时间 身份证 IP地址 URL Email地址 中文字符 对于同一个目的,正则表达式往往有多种写法,大多没有唯一正确的写法,本节的写法主要是示例.此外,写一个正则表达式,匹配希望匹配的内容往往比较容易,但让它不匹配不希望匹配的内容,则往往比较困难,也就是说,保证精确性经常是很难的,不过,很多时候,我们也没有必要写完全精确的

计算机程序的思维逻辑 (37) - 泛型 (下) - 细节和局限性

35节介绍了泛型的基本概念和原理,上节介绍了泛型中的通配符,本节来介绍泛型中的一些细节和局限性. 这些局限性主要与Java的实现机制有关,Java中,泛型是通过类型擦除来实现的,类型参数在编译时会被替换为Object,运行时Java虚拟机不知道泛型这回事,这带来了很多局限性,其中有的部分是比较容易理解的,有的则是非常违反直觉的. 一项技术,往往只有理解了其局限性,我们才算是真正理解了它,才能更好的应用它. 下面,我们将从以下几个方面来介绍这些细节和局限性: 使用泛型类.方法和接口 定义泛型类.方

计算机程序的思维逻辑 (7) - 如何从乱码中恢复 (下)?

乱码 上节说到乱码出现的主要原因,即在进行编码转换的时候,如果将原来的编码识别错了,并进行了转换,就会发生乱码,而且这时候无论怎么切换查看编码的方式,都是不行的. 我们来看一个这种错误转换后的乱码,还是用上节的例子,二进制是(16进制表示):C3 80 C3 8F C3 82 C3 AD,无论按哪种编码解析看上去都是乱码: UTF-8 à??í Windows-1252 ?€?????- GB18030 脌脧脗铆 Big5 ???穩 虽然有这么多形式,但我们看到的乱码形式很可能是"à??í&qu