ZB-035-01Java的类与Class

Java的类与Class

先看一个例子

Animal.java

1
public class Animal {}

Cat.java

1
2
3
public class Cat extends Animal {
public void catchMouse(){}
}

WhiteCat.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WhiteCat extends Cat {
public static void main(String[] args) {

Object obj = new WhiteCat();
// 为什么可以直接赋值给 Object?

/*
这就是 继承啊
因为在java世界里 任何的类都继承了 Object 类,它是整个java世界的基础(根)
所以 java中 任何的类都是 Object的子类
*/

// obj.catchMouse();
// 为什么报错
/*
因为 java 是一个强类型语言,因此
在 Object obj = new WhiteCat(); 时 obj 是 Object类型的
因此编译时 Object 没有 catchMouse() 所以会报错
*/
}
}

RTTI(Run-Time Type Identification) 运行时类型识别

在任何时刻,任何一个对象都清楚的知道自己是什么类型的。

Object.getClass()

在整个 java 运行期间 任何一个对象都可以在任何时候知道自己是什么类型的

1
2
3
4
5
6
7
8
9
10
11
// 这个方法就是 
// obj.getClass();

public class WhiteCat extends Cat {
public static void main(String[] args) {

Object obj = new WhiteCat();
System.out.println(obj.getClass());
// class com.io.demo.WhiteCat
}
}

一个Class 对象就是一个类的说明书

Class 和 类的关系

什么是 JVM ? 就是一个虚拟的计算机,所有的java代码都可以在里面跑。

我们平常写的 java代码是什么

(源代码)xxx.java 写完后经过 编译变成 xxx.class (也就是通常所说的字节码) 它是java跨平台的基石

jvm 可以跑 xxx.class 这样就实现了 一处编写,处处运行

JVM里有什么

  • 栈 (方法栈)
  • 堆 java中 所有的对象都是在 堆里 分配的
    • new Cat() 的过程就是在 堆里创建

Cat 是如何创建的呢?

我想创建一个 Cat,它长什么样子有什么属性,这些东西存在那里?

这个信息就在堆里一个很奇怪的地方存着,叫做 class文件

一个Class对象就是一个类的说明书

1
2
3
4
5
6
7
8
9
10
11
public class Cat extends Animal {
public String[] leg;
public String head;
public String tail;

// 不和任何实例变量相绑定,它是归属于 Cat.class这个说明书的
// 因为所有的 Cat 类实例都共享同一份说明书
public static int count;

public void catchMouse(){}
}

此时当你 new Cat() 时候,JVM就会根据 Cat的说明书 把它创建出来。

1
2
3
4
5
// 于是你疯狂造猫
new Cat()
new Cat()
new Cat()
。。。

这些猫 有什么共同点呢?

  • 都是同一份说明书上生产出来的
  • 每只猫都有独立的属性 leg / head / tail
  • public static int count 本质是什么 归属于 这份说明书的,每个 cat对象共享这份说明书。

这个Cat的说明书是什么?

  • Class对象
  • 静态变量归属于这个 Class对象

instanceof 瞬间豁然开朗

1
2
3
4
5
6
obj instanceof Cat

通过 obj.getClass()
知道每个对象到底是什么类型的

因此它当然清楚知道 它到底是什么类型的

刚刚的Cat说明书在jvm内部开辟的一小块空间

它是非常特殊的,因为所有的类型基本上都只存在一份说明书

这个东西在 java7之前 叫做 永久代

java8之后叫做 元空间

将来你在工作的某一天迟早会碰到这样一个异常

  • Java7之前叫做 OutOfMemory:PermGen
  • Java8之后 叫做 MetaSpace

当你看到它们的时候,基本上是你的类出问题了

总结

每个对象创建出来的时候都需要一份说明书。

一个Class对象就是一份说明书,

因此你总是可以问一个对象obj.getClass() 你是从哪里装配出来的

然后对象说,我是从这个类 装配出来的,这就是 Class 文件的本质。

因此 你很容易实现 instanceof 判断

强制类型转换

1
2
3
4
5
6
Object obj = new WhiteCat();
Integer i = (Integer) obj;

// 此时你会得到一个异常
Exception in thread "main" java.lang.ClassCastException: com.io.demo.WhiteCat cannot be cast to java.lang.Integer
at com.io.demo.WhiteCat.main(WhiteCat.java:7)

为什么报错?

首先强制类型转换本来就是不安全的,编译器允许你这样做

但是它在运行时会出问题,因为每个对象都清楚的知道自己是从什么对象创建出来的。

所以会报错

ZB-034-02字符串和编码

字符串和编码

  • ⼀个⼈类世界中的字符如何转换成计算机世界中的字节? 如 “一”
    • ⼀定在某个神奇的⻆落存在⼀种映射关系
    • 我们称这种映射关系为 字符集
    • 将⼈类能看懂的字符变成字节,叫做 编码
    • 将字节变成⼈类能看懂的字符,叫做 解码

字符集:Unicode

  • 每个数字代表⼀个字符,叫做“码点”(code point)
  • 那我能不能直接拿来变成字节啊?
    • 可以……但是没必要。
  • 最常⻅的两种编码⽅案:
    • UTF-16:Java程序内部的存储⽅法
    • UTF-8 (发明者是 ken)
      • Mac/Linux默认编码是UTF-8
      • Windows默认的中⽂编码是GBK
        • 为什么 windows 不用 UTF-8呢?
        • 答案是: windows 在使用时 UTF-8还没发明
      • 如果没有意外,把你所有的编码⽅案都改成UTF-8(支持度最好的解决方案)

pom.xml 里的字符集设置

1
2
3
4
5
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Google 小技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 只在 当前站内搜索
site:github.com


# 排除 某站的内容
java -csdn

# 搜索 内容必须是连在一起的
"java string"

# 搜指定格式
重构改善现有代码 filetype:pdf

# 常用数据
名单 filetype:xls

推荐读物

  • 《code》编码

练习

ZB-034-01字符串原理

Java字符串详解

字符串是最重要的引用类型之一

  • 互联网基本只干一件事:处理字符串
    • 你随便打开一个网址
    • 为什么很少听说 拿 C/C++ 去写一个互联网应用 ,因为 它们的字符串处理简直是灾难
  • 能处理好字符串是web服务器的基本要求
    • Php
    • Python
    • Java
    • Ruby

非常推荐把 Java的 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

/**
* The {@code String} class represents character strings. All
* string literals in Java programs, such as {@code "abc"}, are
* implemented as instances of this class.
* <p>
* Strings are constant; their values cannot be changed after they
* are created. String buffers support mutable strings.
* Because String objects are immutable they can be shared. For example:
* <blockquote><pre>
* String str = "abc";
* </pre></blockquote><p>
* is equivalent to:
* <blockquote><pre>
* char data[] = {'a', 'b', 'c'};
* String str = new String(data);
* </pre></blockquote><p>
* Here are some more examples of how strings can be used:
* <blockquote><pre>
* System.out.println("abc");
* String cde = "cde";
* System.out.println("abc" + cde);
* String c = "abc".substring(2,3);
* String d = cde.substring(1, 2);
* </pre></blockquote>
* <p>
* The class {@code String} includes methods for examining
* individual characters of the sequence, for comparing strings, for
* searching strings, for extracting substrings, and for creating a
* copy of a string with all characters translated to uppercase or to
* lowercase. Case mapping is based on the Unicode Standard version
* specified by the {@link java.lang.Character Character} class.
* <p>
* The Java language provides special support for the string
* concatenation operator (&nbsp;+&nbsp;), and for conversion of
* other objects to strings. String concatenation is implemented
* through the {@code StringBuilder}(or {@code StringBuffer})
* class and its {@code append} method.
* String conversions are implemented through the method
* {@code toString}, defined by {@code Object} and
* inherited by all classes in Java. For additional information on
* string concatenation and conversion, see Gosling, Joy, and Steele,
* <i>The Java Language Specification</i>.
*
* <p> Unless otherwise noted, passing a <tt>null</tt> argument to a constructor
* or method in this class will cause a {@link NullPointerException} to be
* thrown.
*
* <p>A {@code String} represents a string in the UTF-16 format
* in which <em>supplementary characters</em> are represented by <em>surrogate
* pairs</em> (see the section <a href="Character.html#unicode">Unicode
* Character Representations</a> in the {@code Character} class for
* more information).
* Index values refer to {@code char} code units, so a supplementary
* character uses two positions in a {@code String}.
* <p>The {@code String} class provides methods for dealing with
* Unicode code points (i.e., characters), in addition to those for
* dealing with Unicode code units (i.e., {@code char} values).
*
* @author Lee Boynton
* @author Arthur van Hoff
* @author Martin Buchholz
* @author Ulf Zibis
* @see java.lang.Object#toString()
* @see java.lang.StringBuffer
* @see java.lang.StringBuilder
* @see java.nio.charset.Charset
* @since JDK1.0
*/

@see 的意思就是为了了解当前的类,你可能需要看看别的类(参考文献/友情链接)

在 IDEA 里 一个对象长什么样 是由它的 toString 决定的

1
2
3
4
5
6
class MyObject{
@Override
public String toString() {
return "hahaha";
}
}

字符串的不可变性(它是引用类型)

  • 为什么字符串是不可变的?
    • 安全:线程安全,存储安全
  • 缺点,每当修改的时候都需要重复创建新的对象

String 为什么不可变

API 层面

  • 它肚子里有个 字符数组的容器 private final char value[]
  • class String 它是 final
  • char value[] 它是 final 的 这个数组不能改变指向
  • String 类中 所有公开的 API 方法中 都没提供改变 value 值的操作,所以 String 是不可变的
1
2
3
4
5
6
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}

不可变的真实原因

  1. 在一个 HashMap 里,它有哈希值,它有若干个 每个桶里存固定的 hashCode
  2. 假如你声明了一个 HashMap<String,Object> 会把这个 String 算一个 hashCode() 会被当作 key 存到 桶里去
  3. Object.hashCode() java世界约定 把一个对象映射成一个整数,方便进行高效的哈希表查找,它有三个约定

    • 同一对象无论何时,在 java的整个生命周期里,必须一致性的返回相同的整数 就因为这个规定,导致String不可变
    • 如果两个对象相等那么必须返回一样的 hashCode
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 代码证明

      String x = "a" // 假设此时 hashCode 是 1
      x = "ab" // 此时我们让他的 HashCode 变吗?
      // 如果变 就违反了 hashCode() 的第一个约定
      // 如果不变 就违反了 第二个约定

      // 所以
      String x = "a" 此时它是一个对象 地址 111
      x = "ab" 此时重新生成了一个对象 地址(引用)从111指向了 200
  4. 为了让我们在哈希桶中 安全的使用 String , String必须不可变

我就要修改 String 怎么办?

千万不要这样

1
2
3
4
5
6
7
String s = "0";
for (int i = 0; i < 10; i++) {
s = s + i;
}

// 每次循环都会创建 零碎的小对象,会给内存带来压力
// 所以这是不可变性带来的缺陷

StringBuilder 字符可变序列

  • 不能当作 Hash桶的 key,因为它是可变的
  • 线程不安全
1
2
3
4
5
StringBuilder s = new StringBuilder("0");
for (int i = 0; i < 10; i++) {
// 可以级联 操作
s.append(i).append("-").append("-");
}

判断回文

1
2
3
System.out.println(
new StringBuilder("ABA").reverse().toString().equals("ABA")
);

StringBuffer

  • 可变
  • 线程安全(有 同步问题 慢),可变字符序列

StringBuffer 和 StringBuilder 区别

  • 它们都可变
  • StringBuffer 线程安全 / StringBuilder 线程不安全

那我要修改怎么办?

  • StringBuilder
    • 优先使⽤,线程不安全,速度快
  • StringBuffer
    • 线程安全,速度相对较慢

字符串的内部构造和常⽤API

  • 不可变性是如何保证的?
  • hash值是如何存储的?

    • 缓存 因为已经不可变了,没必要重新生成
    • String 源码里 的 hash 变量 就是用来缓存 hash值的
      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
      /** Cache the hash code for the string */
      private int hash; // Default to 0 默认是0

      public int hashCode() {
      int h = hash;
      // 如果 hash 是默认值0 就计算一下,不等于0 代表已经计算过了
      if (h == 0 && value.length > 0) {
      char val[] = value;

      for (int i = 0; i < value.length; i++) {
      h = 31 * h + val[i];
      }
      hash = h;
      }
      return h;
      }
      /*
      第一次访问的时候 hash=0(默认值) ,
      然后它会被创建出来 hash 被计算出来 ,以后在被访问就直接用
      这就是 hashCode 在 String 中的实现


      重点来了
      第一次访问的时候 它会被创建出来 ,以后在被访问就直接用

      如果多线程呢?
      潜在问题 假如两个线程 同时访问 时候 当前这俩都是 hash = 0
      然后同时计算,并创建 hash = xxx

      虽然有线程安全问题,但是不要紧
      因为 即使存在100个线程同时访问,但是 算出的结果都是一样的

      虽然执行过程不是线程安全的,但是 算出的 hash 结果一致。
      */
  • 我如何使⽤字符串随⼼所欲地完成⼯作?

  • 还不够的,StringUtils补上

ZB-032-03Stream的Collector

Collector 操作

Collector 与 Collectors

Collectors API

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
*
* <pre>{@code
* // Accumulate names into a List
* List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
*
* // Accumulate names into a TreeSet
* Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));
*
* // Convert elements to strings and concatenate them, separated by commas
* String joined = things.stream()
* .map(Object::toString)
* .collect(Collectors.joining(", "));
*
* // Compute sum of salaries of employee
* int total = employees.stream()
* .collect(Collectors.summingInt(Employee::getSalary)));
*
* // Group employees by department
* Map<Department, List<Employee>> byDept
* = employees.stream()
* .collect(Collectors.groupingBy(Employee::getDepartment));
*
* // Compute sum of salaries by department
* Map<Department, Integer> totalByDept
* = employees.stream()
* .collect(Collectors.groupingBy(Employee::getDepartment,
* Collectors.summingInt(Employee::getSalary)));
*
* // Partition students into passing and failing
* Map<Boolean, List<Student>> passingFailing =
* students.stream()
* .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
*
* }</pre>
  • collector 操作是最强⼤的操作
  • toSet/toList/toCollection
  • joining()
  • toMap()
  • groupingBy()

把结果收集到 TreeSet

1
2
3
4
5
6
7
List<User> users = Arrays.asList(new User("张三",20),new User("张三疯",15),new User("李四",100));

TreeSet<String> result = users.stream().filter(user->user.name.startsWith("张"))
.sorted(Comparator.comparing(User::getAge))
.map(User::getName)
// 重点就是 这句
.collect(Collectors.toCollection(TreeSet::new));

Collectors.groupingBy 将User 按照部门分组

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Arrays;
import java.util.stream.Collectors;

public class Problem4 {
// 再用流的方法把之前的题目做一遍吧:
// 请编写一个方法,对传入的List<Employee>进行如下处理:
// 返回一个从部门名到这个部门的所有用户的映射。同一个部门的用户按照年龄进行从小到大排序。
// 例如,传入的employees是[{name=张三, department=技术部, age=40 }, {name=李四, department=技术部, age=30 },
// {name=王五, department=市场部, age=40 }]
// 返回如下映射:
// 技术部 -> [{name=李四, department=技术部, age=30 }, {name=张三, department=技术部, age=40 }]
// 市场部 -> [{name=王五, department=市场部, age=40 }]
public static Map<String, List<Employee>> collect(List<Employee> employees) {
return employees.stream()
.sorted(Comparator.comparing(Employee::getAge))
.collect(Collectors.groupingBy(Employee::getDepartment));
}

public static void main(String[] args) {
System.out.println(
collect(
Arrays.asList(
new Employee(1, "张三", 40, "技术部"),
new Employee(2, "李四", 30, "技术部"),
new Employee(3, "王五", 40, "市场部"))));
}

static class Employee {
// 用户的id
private final Integer id;
// 用户的姓名
private final String name;
// 用户的年龄
private final int age;
// 用户的部门,例如"技术部"/"市场部"
private final String department;

Employee(Integer id, String name, int age, String department) {
this.id = id;
this.name = name;
this.age = age;
this.department = department;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public String getDepartment() {
return department;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Employee person = (Employee) o;
return Objects.equals(id, person.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", department='" + department + '\'' +
'}';
}
}
}

Collectors.joining把流中元素以 “,” 连接为一个 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;

public class Problem6 {
// 使用流的方法,把所有长度等于1的单词挑出来,然后用逗号连接起来
// 例如,传入参数words=['a','bb','ccc','d','e']
// 返回字符串a,d,e
public static String filterThenConcat(Set<String> words) {
return words.stream()
.filter(word->word.length()==1)
.collect(Collectors.joining(","));
}

public static void main(String[] args) {
System.out.println(filterThenConcat(new LinkedHashSet<>(Arrays.asList("a", "bb", "ccc", "d", "e"))));
}
}

并发流

  • parallelStream()
  • 可以通过并发提⾼互相独⽴的操作的性能
  • 在正确使⽤的前提下,可以获得近似线性的性能提升
  • 但是!使⽤要⼩⼼,性能要测试,如果你不知道⾃⼰在做什
    么,就忘了它吧。

统计 1~1000的质数

  • 我们可以把它分为两个部分 [1~500] [501~1000]
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
37
38
39
40
package com.io.demo;

import java.util.stream.IntStream;

public class Xxx {
public static void main(String[] args) {
long t0 = System.currentTimeMillis();
IntStream.range(1,1000_000).filter(Xxx::isPrime).count();

System.out.println(System.currentTimeMillis() - t0);

long t1 = System.currentTimeMillis();
IntStream.range(1,1000_000).parallel().filter(Xxx::isPrime).count();

System.out.println(System.currentTimeMillis() - t1);
}

public static boolean isPrime(int num){
double sqrt = Math.sqrt(num);
if (num < 2) {
return false;
}
if (num == 2 || num == 3) {
return true;
}
if (num % 2 == 0) {// 先判断是否为偶数,若偶数就直接结束程序
return false;
}
for (int i = 3; i <= sqrt; i+=2) {
if (num % i == 0) {
return false;
}
}
return true;
}
}

// 424
// 81
// 性能差距非常大

练习

参考⽂献

  • Effective Java Item 42-48 (专门讲stream)

ZB-032-02Stream的API

Stream的API-创建Stream

  • Collection.stream() 最常用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List<User> users = Arrays.asList(new User("张三",20),new User("张三疯",15),new User("李四",100));

    // 创建一个流
    // users.stream()

    users.stream().filter(user->user.name.startsWith("张"))
    .sorted(Comparator.comparing(User::getAge))
    .map(User::getName)
    .collect(Collectors.toList());
  • Stream.of(T … values) 接收若干个参数 把它变成一个流

  • String.chars() 把这个字符串包含的所有字符变成一个流
  • IntStream.range()
    1
    IntStream.range(0,2) // 包头不包尾

API

  • boolean allMatch(Predicate<? super T> predicate); 是否所有元素符合这个判定
  • boolean anyMatch(Predicate<? super T> predicate); 是否任何一个元素符合这个判定
  • noneMatch 任何元素都不满足这个判定
  • count() 统计个数
  • Stream<T> distinct(); 去重
  • empty() 清空流
  • filter 过滤
  • findAny 看看有没有任何一个元素在这个流里面
  • generate 返回一个无限的 类似生成器 “低频使用”
  • limit 限制这个流的长度
  • map 把一个流映射为另外一个流
  • max
  • min
  • of 创建一个流
  • peek 看看流最开始的元素是什么
  • reduce 把这个流变成新的元素
  • skip 跟 limit 很像
  • sorted 把流变成有序的流
  • toArray
  • findFirst/findAny

String.chars

统计大写字母次数

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.github.hcsp.polymorphism;

public class Main2 {
public static void main(String[] args) {

int count = printUpperCaseLetters("ABCabcDEFdef");
System.out.println(count); // 6
}

public static int printUpperCaseLetters(String str){
return (int)str.chars().filter(Character::isUpperCase).count();
}
}

IntStream

返回 1 ~10 之间的偶数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.stream.IntStream;

public class Main2 {
public static void main(String[] args) {

printOddNumbersBetween(1,10);
}

public static void printOddNumbersBetween(int start,int end){
// 包头不包尾
// IntStream.range(0,2); // 0 , 1
IntStream.range(start, end + 1)
.filter(i -> i % 2 == 0) // 过滤
.mapToObj(i -> "加工后的数字为:" + i) // 对每个数据进行 一个映射 如 从 int --> String
.forEach(System.out::println); // 终结操作

}
}

anyMatch

是否含有姓 李 的用户

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
37
38
39
40
import java.util.*;
import java.util.stream.Collectors;

public class Main {
static class User{
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

public static void main(String[] args) {
List<User> users = Arrays.asList(new User("张三",20),new User("张三疯",15),new User("李四",100));

// 获取姓 李 的用户
boolean hasLi = users
.stream()
.anyMatch(user->user.getName().startsWith("李"));
System.out.println(hasLi);
}
}

findAny

1
2
3
users.stream()
.filter(user -> user.getName().startsWith("张"))
.findAny();

联合 Optional 的正确使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 public static void findZhangUser(){
List<User> users = Arrays.asList(new User("张三",20),new User("张三疯",15),new User("李四",100));

Optional<User> optionalUser = users.stream().filter(user -> user.getName().startsWith("张")).findAny();

// 最佳实践 Optional + 函数式
// 正确写法:不存在抛出错误
optionalUser.orElseThrow(IllegalStateException::new);
// 存在则打印
optionalUser.isPresent(System.out::println);

// 不好的写法 ,optionalUser这个用户存在就打印
if(optionalUser.isPresent()){
System.out.println(optionalUser.get().getName());
}else{
throw new IllegalStateException();
}
}

级联操作 Optional + 函数式的最佳用法

  • orElseThrow 参数是一个 Supplier 从无到有
  • isPresent 参数是 Consumer 接受一个东西把它吃掉
  • 不要把 Optional 当作空指针的替代品,当作参数传来传去
  • 应该把它当作返回值 对这个返回值进行函数式操作
1
2
3
4
5
6
7
 public static void findZhangUser(){
List<User> users = Arrays.asList(new User("张三",20),new User("张三疯",15),new User("李四",100));
// 最佳实践 Optional + 函数式
User zhang = users.stream().filter(user -> user.getName().startsWith("张")).findAny().orElseThrow(IllegalStateException::new);
System.out.println(zhang.getName());

}

ZB-032-01Stream入门

Stream

什么是Stream

  • ⼀个“流”
  • 不易出错
  • 简化代码
  • 可读性/可维护性++

Stream的API-中间操作

  • 仍然返回Stream的操作
  • filter
  • map
  • sorted

Stream的API-终结操作

  • 返回⾮Stream的操作,包括void
  • ⼀个流只能被消费⼀次
  • forEach
  • count/max/min
  • findFirst/findAny
  • anyMatch/noneMatch
  • ⭐collect

例子

  • 过滤姓张的用户
  • 按年龄排序
  • 把它们的名字汇总到 List 里
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import java.util.*;
import java.util.stream.Collectors;

public class Main {
static class User{
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

public static void main(String[] args) {
// 把姓张的用户 挑出来,按照年龄排序,然后把它们的 名字 报告给我
List<User> users = Arrays.asList(new User("张三",20),new User("张三疯",15),new User("李四",100));

// java 7
List<String> zhangUsers = getZhangUsers(users);

// java8 stream来实现
//创建 stream ===> users.stream() 把 users 里的内容放在 河流里
// 从上到下流淌着 创建流之后我们可以进行若干个中间操作
/*
中间操作有什么? 中间操作仍然返回一个流
filter(接收一个 Predicate)
map 把一个流内(布满了 User user) 转换为 另一个流(如 布满了 String name) 传递一个 从一个类型到另一个类型的 抽象的函数映射
sorted
*/
// 过滤姓张的
//users.stream().filter(user->user.name.startsWith("张"))

// 按照年龄排序
// users.stream().filter(user->user.name.startsWith("张")).sorted(Comparator.comparing(User::getAge))

// 生成 name的映射
// users.stream().filter(user->user.name.startsWith("张")).sorted(Comparator.comparing(User::getAge)).map(User::getName)

// 将河流的内容 返回到集合里
// 终结操作 只能有一个用完就销毁了 ,一个流只能消费一次
// 所有返回非 Stream 的操作 包括 void 都是终结操作
/*
forEach
count/max/min
findFirst/FindAny
anyMatch/noneMatch
collect
*/


// collect
users.stream().filter(user->user.name.startsWith("张"))
.sorted(Comparator.comparing(User::getAge))
.map(User::getName)
.collect(Collectors.toList());

/*
过滤后的 stream 会被 塞回 原 list吗? 不会
stream 只能被消费一次,一旦被消费,就没了,即使你保存之前的引用
*/
}

public static List<String> getZhangUsers(List<User> users){
List<User> zhangUsers = new ArrayList<>();

for (User user: users) {
if(user.name.startsWith("张")){
zhangUsers.add(user);
}
}
Collections.sort(zhangUsers, new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
if(o1.age - o2.age < 0){
return -1;
}else if(o1.age - o2.age > 0){
return 1;
}else {
return 0;
}
}
});

List<String> names = new ArrayList<>();

for (User user:zhangUsers) {
names.add(user.name);
}
return names;
}
}

IDEA Stream插件

  • 搜索 stream

debug的时候 多了一个小图标 Trace Current Stream Chain (跟踪当前的流链)

参考链接

ZB-031-java8函数式编程01

为什么要使用函数式编程

  • 减少工作量
  • 提高效率
  • 减少bug

条件筛选用户实例

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class User {
/** 用户ID,数据库主键,全局唯一 */
private final Integer id;

/** 用户名 */
private final String name;

public User(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

// 过滤ID为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.id % 2 == 0) {
results.add(user);
}
}
return results;
}

// 过滤姓张的用户
public static List<User> filterZhangUsers(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("张")) {
results.add(user);
}
}
return results;
}

// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("王")) {
results.add(user);
}
}
return results;
}
// 你可以发现,在上面三个函数中包含大量的重复代码。
public static List<User> filter(List<User> users, Predicate<User> predicate) {}
}

可以看到非常麻烦!!!代码非常的重复

先用接口来实现这个功能

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
public class User {
// ...
private interface 判断条件是否成立 {
boolean 这个用户是否满足条件(User user);
}

private static class 用户ID是偶数的条件 implements 判断条件是否成立{
@Override
public boolean 这个用户是否满足条件(User user) {
return user.id % 2 == 0;
}
}

public static List<User> filter(List<User> users, 判断条件是否成立 条件) {
List<User> results = new ArrayList<>();
for (User user: users) {
if(条件.这个用户是否满足条件(user)){
results.add(user);
}
}
return results;
}

public static void main(String[] args) {
List<User> res0 = filter(Arrays.asList(new User(1,"a"),new User(2,"b")),new 用户ID是偶数的条件());
System.out.println(res0.get(0).id);
}
}

不同条件都去实现一个类太麻烦

简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<User> res0 = filter(Arrays.asList(new User(1,"a"),new User(2,"b")),new 用户ID是偶数的条件());
System.out.println(res0.get(0).id);

// 简化——匿名类实现接口
List<User> res1 = filter(Arrays.asList(new User(1,"a"),new User(2,"b")),new 判断条件是否成立(){
@Override
public boolean 这个用户是否满足条件(User user) {
return user.id % 2 == 0;
}
});
System.out.println(res1.get(0).id);

// 再次简化—— lambda 表达式
List<User> res2 = filter(Arrays.asList(new User(1,"a"),new User(2,"b")), user -> user.id % 2 == 0);
System.out.println(res2.get(0).id);

Predicate

回忆高中数学的函数概念y = f(x)

从 x 到 y 的映射

1
2
3
4
5
6
public interface Predicate<T> {
// 将其他类型返回一个 boolean 类型的结果
boolean test(T t);

...
}

Predicate来简化

  • test方法实际就是 User 到 boolean 的映射
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class User {
// ...
public static List<User> filter(List<User> users, Predicate<User> predicate) {
List<User> results = new ArrayList<>();
for (User user: users) {
if(predicate.test(user)){
results.add(user);
}
}
return results;
}

public static void main(String[] args) {
List<User> res = filterUsersWithEvenId(Arrays.asList(new User(1,"a"),new User(2,"b")));
System.out.println(res);

List<User> res2 = filterZhangUsers(Arrays.asList(new User(1,"张三"),new User(2,"李四")));
System.out.println(res2);
}

// 过滤ID为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.id % 2 == 0 ;
}
});
}

public static List<User> filterUsersWithEvenId2(List<User> users) {
return filter(users, user -> user.id % 2 == 0);
}

// 奇怪的语法,因为User.userWithEvenId 是一个静态方法
public static List<User> filterUsersWithEvenId3(List<User> users) {
return filter(users, User::userWithEvenId);
}

// 静态方法 不和实例相绑定
public static boolean userWithEvenId(User user){
return user.getId() % 2 == 0;
}

// 过滤姓张的用户 lambda 表达式
public static List<User> filterZhangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("张"));
}


public boolean isWang(){
return this.name.startsWith("王");
}

// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("王"));
}

// 实例方法 默认传递了 this
public static List<User> filterWangUsers2(List<User> users) {
return filter(users, User::isWang);
}
}

lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static List<User> filterUsersWithEvenId(List<User> users) {
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.id % 2 == 0 ;
}
});
}
// 这里的 user -> user.id % 2 == 0 就是 lambda表达式
// 实际是这样 (User user) -> user.id % 2 == 0
// 当只有一个参数的时候 它有类型推断 你可以直接省略为如下格式
public static List<User> filterUsersWithEvenId(List<User> users) {
return filter(users, user -> user.id % 2 == 0);
}

为什么这里可以把 Predicate 简化为 lambda表达式呢?

  • 说白了 test 就是一个函数
  • 任何满足满足从 T 到 boolean 值的映射 都可以自动被转换为一个函数接口

什么是函数接口?

  • Predicate 源码上有一个注解@FunctionInterface 它只是一个标记,不是必要的即使去掉也可以正常工作

三种方式 Predicate

  • lambda 表达式
    • user -> user.name.startsWith("王")
  • static 方法

    1
    2
    3
    static boolean xx(User user){...}

    User::xx
  • 实例方法 instance method

    1
    2
    3
    boolean xxx(){}

    User::xxx

函数接口详解

什么东西可以自动被转换为函数接口呢?

  • 任何只包含一个抽象方法的接口都可以被自动转换为函数接口
    • Predicate 只有一个 test 是抽象方法,其他都有方法体
    • 如我们自己实现一个
      1
      2
      3
      interface 阿猫阿狗 {
      boolean 吃骨头(Animal animal);
      }

Consumer

输入是个 User 输出是一个 void(虚空)

1
2
3
4
5
6
7
8
9
10
11
12
List<User> users = Arrays.asList(new User(1,"a"),new User(2,"b")); 
// Consumer
users.forEach(new Consumer<User>(){
@Override
public void accept(User user){
System.out.println(user);
}
})
// 简化
users.forEach(user -> System.out.println(user));
// 再次简化
users.forEach(System.out::println);

Function (类似js的map函数)

把一个类型User变换成另一个类型String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String getName() {
// 这里有个隐式的指针 this
return name;
}

public static String mapUserToString(User user, Function<User,String> function){
return function.apply(user);
}

public static void main(String[] args) {
mapUserToString(new User(1,"aaa"),user -> user.getName());
// getName 满足 从 User => String 的映射
mapUserToString(new User(1,"aaa"), User::getName);
}

Supplier

Consumer 的逆操作

  • Consumer 是把一个东西吃掉 消费掉
  • Supplier 是从虚空变成一个东西

从 void ===> Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 从虚空 变出一个对象
create(()->new Object());
// alt+ enter
create(Object::new);

// 从虚空 变出一个对象
create(()->"");
// 从虚空 变出一个对象
create(()->new User(1,"abc"));

}

public static Object create(Supplier<Object> supplier){
return supplier.get();
}

其他Function。。。

  • BiConsumer (一次吃两个)
  • BiFunction (两个东西映射成一个东西)
  • BinaryOperator (二元操作符)
  • BiPredicate
  • BooleanSupplier 从boolean 变出一个东西
  • ToIntBiFunction
  • UnaryOperator
  • 。。。

函数式实践

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.github.hcsp.polymorphism;

import java.io.IOException;
import java.util.*;

public class Point implements Comparable<Point>{

private final int x;
private final int y;
// 代表笛卡尔坐标系中的一个点
public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public int getY() {
return y;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

Point point = (Point) o;

if (x != point.x) {
return false;
}
return y == point.y;
}

@Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}

@Override
public String toString() {
return String.format("(%d,%d)", x, y);
}

// 按照先x再y,从小到大的顺序排序
// 例如排序后的结果应该是 (-1, 1) (1, -1) (2, -1) (2, 0) (2, 1)
public static List<Point> sort(List<Point> points) {
Collections.sort(points);
return points;
}

public static void main(String[] args) throws IOException {
List<Point> points =
Arrays.asList(
new Point(2, 0),
new Point(-1, 1),
new Point(1, -1),
new Point(2, 1),
new Point(2, -1));
System.out.println(Point.sort(points));


// 传统方式 字节写实现比较器细节
Collections.sort(points, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
if(o1.x < o2.x){
return 1;
}else if (o1.x > o2.x){
return -1;
}

if(o1.y <o2.y){
return 1;
}else if (o1.y > o2.y){
return -1;
}

return 0;
}
});

// java8之后
// java8之后
// java8之后

// 先 x 后 y
Collections.sort(points,Comparator.comparing(Point::getX).thenComparing(Point::getY));

// 先 x 反序 后 y
Collections.sort(points,Comparator.comparing(Point::getX).reversed().thenComparing(Point::getY));

//先 x 反序 后 y 反序
Collections.sort(points,Comparator.comparing(Point::getX).
reversed()
.thenComparing(Comparator.comparing(Point::getY).reversed()));
}

@Override
public int compareTo(Point that) {
// 比较 this 和 that
if(this.x < that.x){
return -1;
}else if(this.x > that.x){
return 1;
}

// 运行到此说明 this.x = that.x
// 因此我要可以进行 y 的排序
if(this.y < that.y){
return -1;
}else if(this.y > that.y){
return 1;
}

return 0;
}
}
  • 详解
1
2
3
4
5
6
7
8
9
// Comparator.comparing 
// 接受一个 Function 参数 从 T类型 到 U类型的映射
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

练习

参考链接

ZB-030-04Maven生命周期详解和插件配置

Spotbugs 自动化字节码bug检查工具

  • 它的前身叫做 findbugs ,但是很不幸的是从 java9以后就不维护了。
  • 它的后续叫做 Spotbugs

引入 Spotbugs

  • 打开链接 Spotbugs 官网
  • 找到 Using SpotBugs
    • 由于我们用的 maven构建 选择 maven
  • 直接复制它的例子代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>3.1.12</version>
    <dependencies>
    <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
    <dependency>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs</artifactId>
    <version>4.0.0-beta3</version>
    </dependency>
    </dependencies>
    </plugin>
  • 继续看文档 spotbugs goal 意思是 “目标” 提示了一个 check命令

  • 然后到你的项目里 运行 mvn spotbugs:check,最后发现我们的代码 Build Success
  • 那就手写一个 bug ,在 main 方法里

    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) throws IOException {
    Integer i = null;
    if(i == 1){
    System.out.println();
    }
    }

    // 很明显会发生空指针异常
  • 再次运行mvn spotbugs:check 但是依然 Build Success

    • 这是因为,你没有给这个插件绑定一个阶段
    • 它分析的是字节码
  • 为 这个 spotbugs指定阶段 , 添加 executions

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>3.1.12</version>
    <dependencies>
    <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
    <dependency>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs</artifactId>
    <version>4.0.0-beta3</version>
    </dependency>
    </dependencies>
    <executions>
    <execution>
    <id>spotbugs</id>
    <phase>verify</phase>
    <goals>
    <goal>check</goal>
    </goals>
    </execution>
    </executions>
    </plugin>
  • 运行 mvn verify 此时给我们丢了一个错误出来

    1
    [ERROR] Null pointer dereference of i in com.github.hcsp.Main.main(String[]) [com.github.hcsp.Main] Dereferenced at Main.java:[line 23] NP_ALWAYS_NULL
  • 继续看 spotbugs 的文档 发现有个gui goal

    • 运行 mvn spotbugs:gui 会出现一个图形界面

一个刚刚的代码疑问

1
2
3
4
5
6
7
8
Integer i = null;
if(i == 1){
System.out.println();
}

// 其中
i == 1 会被处理成 i.equals(1) 吗?
如果你是写 java, 在 java中 == 永远不会自动变成 .equals()

Maven详解

它有一套生命周期 你可以搜索 maven lifecycle

  • 它会把这个生命周期的流程 从头到尾 执行一遍

这些生命周期的阶段会执行什么呢?

  • 默认情况什么都不做,除非你告诉它做什么

怎么告诉?通过另外一个机制

  • 插件 plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mvn
|validate |
| |
|... |
|compile--|--< 默认插件 maven-compiler-plugin
绑定到compile 阶段
于是执行此工作,这个工作叫做 goal 目标
| |
|test ---|--> surefile 官方测试插件 自动绑定到test阶段
| |
|... | 自定义插件 maven-checkstyle-plugin :
你可以指定绑定的阶段
| |
|verify |

自定义插件 指定阶段

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<configLocation>${basedir}/.circleci/checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<enableRulesSummary>false</enableRulesSummary>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.22</version>
</dependency>
</dependencies>
</plugin>

重点看 execution
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
意思是 这个插件有个 goal 是 check ,阶段绑定在 compile 阶段
因此 maven运行在 compile阶段的时候,就知道要执行它

如果我绑定两个阶段呢?会先执行谁呢?
谁先声明的绑定谁

你还可以把它修改为 verify 阶段,这样 在 compile 阶段就不会执行它了
<execution>
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>

可不可以在 compile 和 verify 阶段都执行?
可以,只要你不觉得费劲,我们允许你做这么蛋疼的事情~~~
为什么允许这样呢? 因为你可以会在不同阶段传递不同的参数
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>

当你执行 mvn verify时 会怎样?

  • 首先 maven 会从头把 lifecycle 的每个阶段从头执行到 verify阶段
  • 每当执行到每个阶段的时候,它会看看这个阶段有没有绑定某个插件的 目标 goal,如果有就执行,没有就什么都不做
  • 这就是 maven 标准,就是maven自动化

ZB-030-03确定算法和重构

确定算法

  • 为什么互联网称为“网”,爬虫被称为“爬虫”
    • 从一个节点出发,遍历所有节点
  • 算法:广度优先算法的一个变体
    • 非常建议自己手写相关算法
      • 广度优先/队列数据结构/JDK的队列实现
  • 如何扩展?
    • 慢慢的把烂代码,啰嗦的代码去掉
    • 假如我想要未来换数据库/上ES
    • 爬虫的通用化

开始实践

新开一个分支

1
git checkout -b basic-algorithm

先来一个死循环

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
37
38
39
public class Main {
public static void main(String[] args) throws IOException {
// 待处理的链接池
List<String> linkPool = new ArrayList<>();

// 已经处理的链接池
Set<String> processedLinks = new HashSet<>();
linkPool.add("https://sina.cn");

while(true){
if(linkPool.isEmpty()){
break;
}

String link = linkPool.get(0);
if(processedLinks.contains(link)){
continue;
}

// 过滤外链
if(!link.contains("sina.cn")){
// 这是我们不感兴趣的不管他
continue;
}else{
// 这是我们感兴趣的,我们只处理新浪站内的链接
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("https://sina.cn/");
try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
System.out.println(EntityUtils.toString(entity1));
}
// 此时需要 jsoup
}

}

}
}

jsoup maven

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.12.1</version>
</dependency>

继续解析网页

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.github.hcsp;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Main {
public static void main(String[] args) throws IOException {
// 待处理的链接池
List<String> linkPool = new ArrayList<>();

// 已经处理的链接池
Set<String> processedLinks = new HashSet<>();
linkPool.add("https://sina.cn");

while(true){
if(linkPool.isEmpty()){
break;
}

// String link = linkPool.get(linkPool.size() - 1);
// // ArrayList从底部删除更有效率
// linkPool.remove(linkPool.size() - 1);

// 简化为一行
String link = linkPool.remove(linkPool.size() - 1);

if(processedLinks.contains(link)){
continue;
}

// 过滤外链
if ( link.contains("news.sina.cn") || "https://sina.cn".equals(link) ) {
// 这是我们感兴趣的,我们只处理新浪站内的链接

System.out.println(link);
if(link.startsWith("//")){
link = "https:" + link;
System.out.println("处理后的link:"+link);
}

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(link);
httpGet.addHeader("User-Agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36");


try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
String html = EntityUtils.toString(entity1);

Document doc = Jsoup.parse(html);

ArrayList<Element> links = doc.select("a");

for(Element aTag:links){
// 丢到链接池里
linkPool.add(aTag.attr("href"));
}

// 假如这是个新闻的详情页就存入数据库,否则,就什么都不做
/*
新闻详情有 标题/内容
*/
ArrayList<Element> articleTags = doc.select("article");
if(!articleTags.isEmpty()){
for (Element articleTag:articleTags){
String title = articleTags.get(0).child(0).text();
System.out.println(title);
}
}

processedLinks.add(link);
}
} else {
// 这是我们不感兴趣的不管他
continue;
}

}

}
}

重构

上面代码里有这样一个条件来筛选我们要的网址

1
2
3
4
5
6
// 过滤外链
if ( (link.contains("news.sina.cn") || "https://sina.cn".equals(link)) && !link.contains("passport.sina.cn") ) {
// 这是我们感兴趣的,我们只处理新浪站内的链接
...
}
`

通常情况下,这么多的条件下

  • 你会写注释,我们不反对写注释,
    • 注释可能会过时,有一天条件变了,但是注释你没改
    • 或者两年后代码不停的变更
    • 一个过时的注释,比不写注释还要糟糕,因为他给了你错误的信息
  • 最聪明的方法是 提取方法并给一个名字
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
private static boolean isIntresetingLink(String link){
return (link.contains("news.sina.cn") || "https://sina.cn".equals(link)) && !link.contains("passport.sina.cn");
}

// 还可以继续简化
private static boolean isIntresetingLink(String link){
return (isNewsPage(link) || isIndexPage(link)) && isNotLoginPage(link);
}


private static boolean isNewsPage(String link){
return link.contains("news.sina.cn");
}

private static boolean isIndexPage(String link){
return "https://sina.cn".equals(link);
}
private static boolean isNotLoginPage(String link){
return !link.contains("passport.sina.cn");
}


// 此时的 if
if(isIntresetingLink(link)){
....
}

每当你要写注释的时候,请先尝试重构,让注释显得多余

继续优化,提取函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static Document httpGetAndParseHtml(String link){
if(link.startsWith("//")){
link = "https:" + link;
System.out.println("处理后的link:"+link);
}

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(link);
httpGet.addHeader("User-Agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36");


try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
String html = EntityUtils.toString(entity1);
return Jsoup.parse(html);
}
}

IDEA重构技巧

  • 选中你的代码块
  • 右键 Refactor -> Extract -> Method -> 写个方法名

三个好处

  • 你是个人类,你大脑处理信息是有限的,因此100行的代码 和10行的小方法,10行的更容易被处理和理解
  • 短的方法容易被复用,越短小的方法越容易复用,而复用所有代码的书写原则,不要重复你自己
  • 对于Java来说你可以对复用方法进行覆盖,实现多态的功能

提交代码

  • 01我们在新分支 basic-algorithm
  • 02提交我们的代码
    • git add .
    • git commit 在vi里写你的信息
    • git push 此时报错了,因为远程仓库没有你的这个分支
    • git push --set-upstream origin basic-algorithm
  • 03打开你的 github 我的是 https://github.com/slTrust/xdml-crawler/

    • 页面会显示一个你最近的提交信息 点击 Compare & pull request 绿色按钮
    • 点击 Create pull request
    • 此时它会开始跑CI ,我们这个代码是会失败的 在 Details里
      • 有任何错误请看 日志的内容 红色的 ERROR
    • 改正我们的代码,再次执行 02 步操作
    • 此时页面刷新以下,多了你最新的 commit , 此时代码通过了检查,意味着我们可以合并了
    • Merge pull request 合并很有讲究的,它有三个选项

      • Create a merge commit
        等价于

        1
        2
        3
        git checkout master
        git merge basic-algorithm
        # 它会产生一个新的提交
      • Squash and merge
        等价于

        1
        2
        3
        git merge --squash
        好处是 比方你这个分支有两个或100个 提交
        他给你压缩成一个提交,这样你回滚的时候非常方便
      • Rebase and merge

        1
        2
        git rebase
        自行尝试
  • 04 此时我们尝试 Squash and merge ,然后点击 Squash and merge

    • 此时弹出一个文本框,意思是 你本来有多个commit 现在压缩成一个 commit
    • 你需要为压扁的这个 commit 定义 提交信息 , 默认是把之前的提交信息列出来
    • 我们稍微修改下信息, 然后点击 Confirm squash and merge
  • 05 此时去看你的仓库的 commit