阅读更多

0顶
1踩

编程语言

原创新闻 Java 8 Stream API 实用指南

2017-11-27 17:17 by 副主编 jihong10102006 评论(1) 有10726人浏览
引用
来源:gitbook
作者:阿福

本文作为 Stream API 的 “使用指南”,主要侧重于 “实用”,并不会关注太多的实现细节,当然,不是简单地罗列接口,而是尽可能地向读者展示 Stream API 的全貌。

开始之前

作为 Java API 的新成员,Stream API “允许以声明式的方式处理数据集合”。回顾 “内容介绍” 部分,我们阅读了以下的代码:
class Good {
  String name;               // 商品名称
  long price;               // 价格
  long sales;               // 销量
  List<String> categories;  // 类别

  // ... 省略 constructor、getter / setter

  // ... 省略 toString
}

void process(List<Good> goods) {
  //
  // 筛选 price > 500 & sales < 200 的商品, 价格最高的 10 件商品, 价格减半(双十一来啦!)
  //
  goods.stream()
    .filter(c -> c.getPrice() > 500 && c.getSales() < 200)
    .sorted(Comparator.comparing(Good::getPrice).reversed())
    .limit(10)
    .forEach(c -> { c.setPrice(c.getPrice() / 2); });
}

即使没有 Stream API,我们依然能够通过完成需求,但无法做到如此简洁、清晰。(本文最后的部分,还会进一步探讨 Stream API 在并发方面的优势。)

开始使用 Stream API 之前,我们需要了解,Stream 是什么?有哪些比较重要的概念?为此,我们针对上文的代码绘制了示意图:

图中所示,整个过程就是将 goods 元素集合作为一个 “序列”,进行一组 “流水线” 操作,其中:
  • goods 集合提供了元素序列的数据源,通过 stream() 方法获得 Stream
  • filter / sorted / limit 进行数据处理,“连接起来” 构成 “流水线”
  • forEach 最终执行
需要说明,filter / sorted / limit 的返回值均为 Stream(类似于 Builder 模式),但它们并不立即执行,而是构成了 “流水线”,直到 forEach:最终执行,并且关闭 Stream。因此:
  • 将 filter / sorted / limited 等能够 “连接起来”,并且返回 Stream 的方法称为 “中间操作”(Intermediate)
  • 将 forEach 等最终执行,并且关闭 Stream 的方法称为 “终止操作” (Terminal)

特别地,需要记住:Stream 的中间操作并不是立即执行,而是 “延迟的”、“按需计算”;并且,完成 “终止操作” 后,Stream 将被关闭。

现在,我们应当了解 Stream 的关键概念:数据源、中间操作构成 “流水线”、终止操作,对于 Stream 的定义,我们直接引用 Java doc:
引用
A sequence of elements supporting sequential and parallel aggregate operations.

package java.util.stream;

public interface Stream<T> extends BaseStream<T, Stream<T>> {
  // ...
}

代码所示:Stream 中序列元素的类型,通过泛型表达。对于原始类似,除了包装类外,Stream API 同时提供了原始类型的 Stream:IntStream、LongStream 以及 DoubleStream

本质而言,Stream API 的 “流水线” 操作,最终仍然依赖于迭代,但与使用 Collection API 直接构建迭代的代码不同,Stream API 通过参数的形式接收我们提供的操作,由其内部实现迭代。

而所谓 “我们提供的操作”,请参考 “Lambda 表达式 & 方法引用”。

Lambda 表达式 & 方法引用

Lambda 表达式,或者 λ,虽然并不是新鲜事物,但其越来越受到重视,尤其是 Java 8(以及 C++11)将 Lambda 表达式纳入标准以后。暂且不谈论 “函数式编程” 的话题,我们先了解 Lambda 表达式如何使用。

Lamba 表达式

对于 Java 开发者,匿名类是很常见的东西,例如:
@FunctionalInterface
interface PriceCalculator {
  long calculate(Good good);
};

public void process(List<Good> goods, PriceCalculator calculator) {
  // 计算商品价格    
}

//
// 实现 PriceCalculator 接口的匿名类实例,作为 process 参数
//
process(goods, new PriceCalculator() {
  @Override
  public long calculate(Good good) {
    return good.getPrice();
  }
});

接口 PriceCalculator 只有一个方法,我们将只有一个抽象方法的接口,称为 “函数式接口”,并以 @FunctionalInterface 进行标记。(注意,Java 8 允许接口提供方法实现,即 “默认方法”,函数式接口必须包含且仅包含一个抽象方法,对于提供实现的默认方法,没有限制)

Lambda 表达式,其本质即为函数式接口的一个实例:
//
// 示例 #1: args -> { statement; } 
//
process(goods, (good) -> {
  return good.getPrice();
});

//
// 示例 #2:args -> expression
//
process(goods, (good) -> good.getPrice());

函数式接口中抽象方法的签名即为 Lambda 表达式的签名,称为 “函数描述符”。Lambda 表达式的类型,由 Java 编译器根据上下文推断获得。

方法引用

方法引用,即为特定情况下 Lambda 表达式的简化,例如:
process(goods, Good::getPrice);

对于 Lambda 表达式到方法引用的简化,我们提供以下规则:
Lambda 表达式 方法引用
(args) -> ClassName.staticMethod(args) ClassName::staticMethod
(arg0, ...) -> arg0.instanceMethod(...) ClassName::instanceMethod
(args) -> expression.instanceMethod(args) expression::instanceMethod

特别的,对于构造函数的方法引用:ClassName::new

开始使用 Stream API

本章节将阐述 Stream 的生成、操作、数据收集,主要介绍 Stream API 的常用接口与辅助方法。为了便于我们试验示例的代码,我们先说明 forEach(Consumer<? super T>)。

正如前面章节所说,forEach(Consumer<? super T>) 是一个 “终止操作”,它遍历 Stream 的元素序列,通过函数式接口 Consumer<? super T> 的 accept(T) 执行特定操作。Consumer<? super T> 的声明:
@FunctionalInterface
public interface Consumer<T> {
  void accept(T);
}

以下的阐述中,将通过 forEach(System.out::println) 将 Stream 的元素序列输出。

生成 Stream

由集合 & 数组生成 Stream

Stream 作为元素的 “序列”,自然而然地,我们想到通过集合、数组生成 Stream。

Java 8 的 Collection 接口添加了 Stream<E> stream() 方法,由集合生成 Stream,例如:
//
// 输出商品集合
//
void print(List<Good> goods) {
  goods.stream().forEach(System.out::println);
}

java.util.Arrays 提供了 stream(T[]) 的静态方法,由 T[] 数组生成 Stream:
//
// 输出商品数组
//
void print(Good[] goods) {
  Arrays.stream(goods).forEach(System.out::println);
}

特别地,当数组元素类型 T 是原始类型,静态方法 stream(T[]) 将返回原始类型的 Stream。

通过集合或数组获得的 Stream,是 “有限” 的。

直接创建 Stream

除了由集合和数组生成 Stream,Stream API 提供了静态方法 Stream.generate(Supplier<T>)、Stream.iterator(final T, final UnaryOperator<T>),直接创建 Stream。

Stream.generate(Supplier<T>) 通过参数 Supplier<T> 获取 Stream 序列的新元素
//
// 生成指定数量的商品并输出
//
void generate(int number) {
  Stream.generate(Good::new).limit(number).forEach(System.out::println);
}


Stream.iterator(final T, final UnaryOperator<T>) 提供了一种 “迭代” 的形式:第一个元素,以及第 n 个元素到第 n + 1 个元素的生成方式 UnaryOperator<T>。
//
// 生成指定数量的序列 1, 2, 4, 8, 16 ... 并输出
//
void generateSequence(int number) {
  Stream.iterate(0, n -> n * 2).limit(number).forEach(System.out::println);
}

通过 Stream.generate(Supplier<T>)、Stream.iterator(final T, final UnaryOperator<T>),将产生 “无限的” Stream,以上的示例中,使用 limit 进行了 Stream 截断。

操作 Stream

filter

filter 是 “中间操作”,以 Predicate<? super T> 的实例作为参数,进行 Stream 过滤,仅保留符合条件的元素。Predicate<? super T> 作为常用的函数式接口,其声明如下:
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T); 
}

例如:
//
// 过滤高于指定价格的商品
//
void filterByPrice(List<Good> goods, long price) {
  goods.stream().filter(c -> c.getPrice() > price).forEach(System.out::println);
}

anyMatch / allMatch / noneMatch

anyMatch、allMatch、noneMatch,都是 “终止操作”,与 filter 接收相同的参数,其功能顾名思义,例如:
//
// 检查商品集合是否包含指定名称的商品
//
boolean hasGoodWithName(List<Good> goods, String name) {
  return goods.stream().anyMatch(c -> name.equals(c.getName()));
}

findAny / findFirst

findAny、findFirst,都是 “终止操作”,分别获取 Stream 元素序列的任意元素和第一个元素:
//
// 获取商品集合中任意名称为指定名称的商品
//
Optional<Good> findAnyGoodWithName(List<Good> goods, String name) {
  return goods.stream().filter(c -> name.equals(c.getName())).findAny();
}

findAny、findFirst 的返回值都是 Optional<T> 类型,避免了 Stream 序列为空时返回 null。关于 Optional<T> 类型,不属于本文的范围,请参阅 Java doc。

相比较于 findFisrt,findAny 更适合于并发的场景。

map

map 是中间操作,将 Stream 序列的元素映射为其他的元素,以 Function<? super T, ? extends R> 作为参数,其声明如下:
@FunctionalInterface
public interface Function<T, R> {
    R apply(T);
}

代码所示,Function<? super T, ? extends R> 提供了 Stream 序列的元素映射为其他元素的途径,例如:
//
// 输出商品的名称
//
void printName(List<Good> goods) {
  goods.stream().map(Good::getName).forEach(System.out::println);
}

此外,Stream 提供 mapToInt、mapToLong、mapToDouble,将 Stream 映射为原始类型 Stream。

flatMap

map 直接将 Stream 序列的元素映射到新的元素,假如 map 映射获得的是 Stream,flatMap 能够将各个 Stream 的元素合并到一个 Stream 中,例如:
//
// 获取商品集合的分类
//
void getCategories(List<Good> goods) {
  goods.stream().flatMap(c -> c.getCategories().stream()).forEach(System.out::println);
}

distinct

distinct 是 “中间操作”,即去重,去重的依据即为 Stream 序列元素类型的 equals 和 hashCode 方法,例如:
//
// 获取商品名称,去重
//
void distinctGoodNames(List<Good> goods) {
  goods.stream().map(Good::getName).distinct().forEach(System.out::println);
}

sorted

sorted 是 “中间操作”,以 Comparator<? super T> 作为参数,将 Stream 序列元素排序,Comparator<? super T>:
//
// 商品按照价格升序排列
//
void sortGoods(List<Good> goods) {
  goods.stream().sorted(Comparator.comparing(Good::getPrice)).forEach(System.out::println);
}

示例代码中,使用辅助方法 Comparator<T> comparing(Function<? super T, ? extends U>) 生成了 Comparator<? super T> 实例。

“内容介绍” 部分的 reversed(),同样是 Comparator<T> 的方法,并提供了默认实现,用于排序时,即可实现排序 “取反”。

limit / skip

limit / skip 是 “中间操作”,接收 long 类型的参数,实现 Stream 序列元素的截取和跳过:
//
// 获得第 page 页的商品,每页商品数量为 page_size
//
void listGoods(List<Good> goods, int page, int page_size) {
  goods.stream().skip((page - 1) * page_size).limit(page).forEach(System.out::println);
}

收集数据


count / min / max
count 是终止操作,将直接返回 Stream 的元素数量:
//
// 获取高于指定价格的商品数量 
//
long countGoodsOverPrice(List<Good> goods, long price) {
  return goods.stream().filter(c -> c.getPrice() > price).count();
}

min / max,以 Comparator<? super T> 作为参数,返回最小值和最大值。对于原始类型 Stream,min / max 无参数,例如:
//
// 获取最高的商品价格
//
OptionalLong maxGoodPrice(List<Good> goods) {
  return goods.stream().mapToLong(Good::getPrice).max();
}

示例代码中,LongStream 的 max 方法返回类型为 OptionalLong,即为原始类型的 Optional<T>。

reduce

reduce,“归约”,是 “终止操作”,用于将 Stream 序列归约到一个具体的值,其声明,如下:
//
// 提供初始值,以及两个 Stream 序列元素结合产生新值的方法
//
T reduce(T, BinaryOperator<T>);

//
// 提供两个 Stream 序列元素结合产生新值的方法,没有初始值,但通过 Optional<T> 避免 Stream 为空时返回 null
//
Optional<T> reduce(BinaryOperator<T>);

//
// 归约到新的类型:提供初始值,新值与 Stream 元素结合的方法,以及两个新值结合的方法
//
<U> U reduce(U, BiFunction<U, ? super T, U>, BinaryOperator<U>);

例如:
long getTotalSalesAmount(List<Good> goods) {
  //
  // 获取 goods 集合的销售总额
  //
  return goods.stream().reduce(0L, (amount, good) -> amount + good.getSales() * good.getPrice(),(left, right) -> left + right);
}

或者:
long getTotalSalesAmount(List<Good> goods) {
  //
  // 获取 goods 集合的销售总额
  //
  return goods.stream().mapToLong(c -> c.getPrice() * c.getSales()).reduce(0, Long::sum);
}

collect

作为 “终止操作”,collect 即 “收集数据”。collect 以 “收集器” Collector<? super T, A, R> 作为参数,通常,我们使用 Collectors 提供的辅助函数获得 “收集器” 实例。

常用的辅助函数

toList / toSet    
    
// // 获取商品名称的集合 // List<String>
    > getGoodNames(List<Good> goods) {   return
    > goods.stream().map(Good::getName).collect(Collectors.toList()); }
    > 

toSet 与 toList 相似,但其返回结果为 Set。

groupingBy

// // 将商品集合按照价格分组 // Map<Long, List<Good>>
    > groupGoodByPrice(List<Good> goods) {   return
    > goods.stream().collect(Collectors.groupingBy(Good::getPrice)); }
    > 
   
partitioningBy

partitioningBy 与 groupingBy 类似,但其得到分组的键类型为 Boolean,即 true & false,最多两组:
// // 根据是否超过指定销量将商品集合分组 // Map<Boolean, List<Good>>
    > partitionGoodWithSales(List<Good> goods, long sales) {   return
    > goods.stream().collect(Collectors.partitioningBy(c -> c.getSales() >=
    > sales)); } 
   
reducing

Collectors.reducing 参数与 Stream.reduce 一致,其获得的 “收集器” 实例,作为 collect 参数,能够与 reduce 获得相同的结果。

根据是否需要存储中间状态,Stream 操作能够划分为 “无状态操作”、“有状态操作”。“无状态操作”,例如:filter、map;“有状态操作”,例如:limit、sorted 等,并且,对于 “有状态操作”,亦根据中间状态存储的要求,其区分 “有界”(例如:limit)、“无界”(例如:sorted)。

并行

通过 “并行 Stream” 即可获得 Stream API 的并行能力,例如:
//
// 获取最高的商品价格
//
OptionalLong maxGoodPrice(List<Good> goods) {
  return goods.stream().parallel().mapToLong(Good::getPrice).max();
}

代码所示,通过 Collection 接口的 parallelStream()、 BaseStream 接口的 parallel() 方法,都能够获得 “并行 Stream”。

并行 Stream 内部是基于 ForkJoinPool 模型获得并行能力,其默认线程数量即为通过 Runtime.getRuntime().availableProcessors() 获得的线程数。

不过,关于并行,两件事必须注意:一方面,正确性,避免 Stream 处理过程中共享可变状态;另一方面,务必记住,并行未必能够提高性能,通常适用于 Stream 元素数量大、或单个元素处理非常耗时的场景。

写在最后

请在阅读完本文后,尝试解答以下问题,最终的答案我们线上交流见 ^_^

1. 以下代码执行,将输出什么?
String[] words = { "a", "bb", "ccc", "dddd", "eeee" };
Arrays.stream(words).filter(c -> { System.out.println(c); return true; }).limit(1).collect(Collectors.toList());

2. 分别运用 Stream API 的 reduce、collect 方法实现以下方法:
long getTotalSalesAmount(List<Good> goods) {
  //
  // 获取 goods 的销售总金额
  //
}

3. 通过 Stream API 实现以下方法:
void printFibonacciSequence(int n) {
  //
  // 输出斐波那契数列的前 n 个数
  //
}

4. 通过 Stream API 改造以下代码: (提示,需要了解 collect 方法的参数类型
Collector<T, A, R>)
class SaleRecord {
    String recordId;  // 销售记录 Id
    int goodId;       // 商品 Id
    int promotionId; // 促销活动 Id
    long price;       // 价格
    long sales;       // 数量
}

class SalesAggregation {
    int goodId;       // 商品 Id
    int promotionId; // 促销活动 Id
    long amount;     // 金额
}

//
// 聚合
//
List<SalesAggregation> aggregate(List<SaleRecord> saleRecords) {
  Map<String, SalesAggregation> salesAggregations = new HashMap<>();
  for (SaleRecord saleRecord : saleRecords) {
    String key = String.format("%d_%d", saleRecord.goodId, saleRecord.promotionId);
    if (salesAggregations.containsKey(key)) {
      salesAggregations.get(key).amount = salesAggregations.get(key).amount + saleRecord.price * saleRecord.sales;
    } else {
      SalesAggregation salesAggregation = new SalesAggregation();
      salesAggregation.goodId = saleRecord.goodId;
      salesAggregation.promotionId = saleRecord.promotionId;
      salesAggregation.amount = saleRecord.price * saleRecord.sales;

      salesAggregations.put(key, salesAggregation);
    }
  }

  return new ArrayList<>(salesAggregations.values());

  • 大小: 3.9 KB
来自: gitbook
0
1
评论 共 1 条 请登录后发表评论
1 楼 gypb 2017-11-30 11:03
整个照抄spark的语法,好样的.

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 中国图书馆图书分类号简表

    方便大家查阅图书分类,只是一个帮助文档   为方便大家写论文或检索文献时查询“中图分类号”,特制作此文档。这是《中国图书分类法》的第四版(最新版)。 声明:原作者不是我,是上海交大的某人!

  • 中图法分类号TP(计算机专业)

    中图法分类号TP类别

  • 计算机相关分类号,中图法分类号(计算机专业) TP

    中图法分类号(计算机专业)T 工业技术TP 自动化技术、计算机技术TP3 计算技术、计算机技术TP3-0 计算机理论与方法TP3-05 计算机与其他学科的关系TP30 一般性问题TP301 理论、方法TP301.1 自动机理论TP301.2 形式语言理论TP301.4 可计算性理论TP301.5 计算复杂性理论TP301.6 算法理论TP302 设计与性能分析TP302.1 总体设计、系统设计TP...

  • 计算机论文分类号 tp,中图法分类号(计算机专业) TP

    中图法分类号(计算机专业)T 工业技术TP 自动化技术、计算机技术TP3 计算技术、计算机技术TP3-0 计算机理论与方法TP3-05 计算机与其他学科的关系TP30 一般性问题TP301 理论、方法TP301.1 自动机理论TP301.2 形式语言理论TP301.4 可计算性理论TP301.5 计算复杂性理论TP301.6 算法理论TP302 设计与性能分析TP302.1 总体设计、系统设计TP...

  • 计算机技术学科分类号,专业分类号及学科码对照表.doc

    专业分类号及学科码对照表PAGEPAGE 4学科代码:学科代码学科名称学科代码学科名称0101哲学类0801地矿类0201经济学类0802材料类0301法学类0803机械类0501中国语言文学类0804仪器仪表类0502外国语言文学类0805能源动力类0503新闻传播学类0806电气工程及信息类(包括电子信息工程、通讯工程、电子科学与技术、计算机科学与技术0504艺术类(包括艺术设计类)0702...

  • 中图法分类号(计算机专业)和文献标识码

    中图法分类号(计算机专业)    T 工业技术 TP 自动化技术、计算机技术  TP3 计算技术、计算机技术   TP3-0 计算机理论与方法    TP3-05 计算机与其他学科的关系   TP30 一般性问题    TP301 理论、方法     TP301.1 自动机理论     TP301.2 形式语言理论     TP301.4 可计算性理论     TP301.5 计算复杂性理论   

  • 计算机类图书按中图法类号,中图分类号 中国图书馆分类法(O类 数理科学和化学)...

    O 数理科学和化学01 数学01-61 数学词典01-64 数学表O1-8 计算工具O11 古典数学O119 中国数学O12 初等数学O121 算术O122 初等代数O123 初等几何O124 三角O13 高等数学O14 数理逻辑、数学基础O15 代数、数论、组合理论O151 代数方程式论、,线性代数O152 群论O153 抽象代数(近世代数)O154 范畴论O155 微分代数、差分代数O156 ...

  • 中图法分类号(计算机,自动化)

    T 工业技术 TP 自动化技术、计算机技术  TP3 计算技术、计算机技术   TP3-0 计算机理论与方法    TP3-05 计算机与其他学科的关系   TP30 一般性问题    TP301 理论、方法     TP301.1 自动机理论     TP301.2 形式语言理论     TP301.4 可计算性理论     TP301.5 计算复杂性理论     TP301.6 算法理论   

  • 中图分类号——计算机软件类

    中图分类号之计算机软件类TP31 计算机软件    TP311 程序设计、软件工程        TP311.1 程序设计            TP311.11 程序设计方法            TP311.12 数据结构            TP311.13 数据库理论与系统                TP311.131 数据库理论                TP311.132 数

  • 中图法计算机技术分类号,中图法分类号(TP3 计算技术、计算机技术)

    TP334.1 终端设备 (显示器入此. 参见TN873.)TP334.2 输入设备 (鼠标入此.)TP334.2+1 图形输入设备 (光笔入此.)TP334.2+2 图像输入设备 (自动扫描仪入此.)TP334.2+3 文字与数字输入设备 (键盘、纸带阅读机、卡片阅读机、光学文字阅读机、光学标记阅读机等入此.)TP334.2+4 语音输入设备TP334.3 输出设备 (兼有输入、输出功能的设备、...

  • 计算机相关分类号,计算机类中图分类号.doc

    文档介绍:TP1 自动化基础理论TP2 自动化技术及设备TP20 一般性问题TP21 自动化元件、部件TP23 自动化装置与设备TP24 机器人技术TP27 自动化系统TP29 自动化技术在各方面的应用TP3 计算技术、计算机技术TP30 一般性问题TP31 计算机软件TP32 一般计算器和计算机TP33 电子数字计算机(不连续作用电子计算机)TP34 电子模拟计算机(连续作用电子计算机)TP35...

  • 学科分类号 计算机技术,学科分类号 0806.DOC

    学科分类号 0806.DOC学科分类号 0806本科生毕业论文(设计)题目(中文): 基于PID神经网络多变量控制系统设计(英文): The Design of PID Neural NetworkMultivariable Control System学生姓名:   李 剑学  号:  0910406024系  别: 物理与信息工程系专  业:   通信工程指导教师:  瞿 军  讲 师...

  • 计算机技术分类

    http://www.hudong.com/categorypage/show/%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%8A%80%E6%9C%AF/ 音频格式 字符编码 KDE 计算机软件 计算机理论与方法 一般性问题 一般计算器和计算机 电子数字计算机 电子模拟计算机 混合电子计算机 微型计算机 多媒体技术与多媒体计算机 其他计算机 计算机的应用计算机技术分类所有词条 ...

  • 计算机类图书的中国法类号,公共书目查询

    1.怎么查询在图书馆借的书是否已经超期?如果是,那应该怎么处理?答:请进入图书馆主页的馆藏书目检索,登录“我的图书馆”进行借阅查询,如果超期,请到总服务台交纳滞纳金,就可以正常借阅了。图书的借期是2个月,如果想续借,直接在“我的图书馆”的借阅查询里点击续借即可。2.图书馆的检索终端的作用?答:答: 图书馆二楼大厅四台检索终端,是提供给读者免费检索图书馆馆藏书刊信息的。3.馆藏目录检索指南答:“...

  • 《中国图书馆图书分类法》(第五版)详表(中图分类号查询表)

    中文法分类

  • 中国图书分类号-自动化_计算机

    《中国图书馆分类法》(原名《中国图书馆图书分类法》,以下简称《中图法》)是我国图书馆和情报单位普遍使用的一部综合性的分类法。到1999年《中图法》已进行四次修订 (对F经济,TN无线电电子学、电信技术,TP自动化技术、计算机技术等三类为重点修订大 类)。 除《中图法》以外,目前我国常用的还有《中国图书资料分类法》(简称《资料法》),《资料法》是在《中图法》的基础上,由中国科学技术信息研究所...

  • 硕士学论文中的国内图书分类号和国际图书分类号

        今天修改了论文封皮里面的国内图书分类号和国际图书分类号,关于这些,简单记录一下吧,希望对于之后同样需要的人提供一点帮助    国内图书分类号相对来数比较容易填写,最简单的方法就是找到跟你内容相似的论文看他们填的什么你也填写同样的内容就行了,如果想自己找的话可以在下面的网址里面去找:    中图分类号查询  中国图书馆分类法   里面类似于目录,是一级一级的形式,你选择一个大的领域之后,会进...

  • 基于springboot+Javaweb的二手图书交易系统源码数据库文档.zip

    基于springboot+Javaweb的二手图书交易系统源码数据库文档.zip

  • Linux课程设计.doc

    Linux课程设计.doc

Global site tag (gtag.js) - Google Analytics