暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

9、Java实战进阶

编码逻辑 2021-08-29
296

方法引用

通过前两个小节对Lambda
表达式的学习,本小节我们来介绍一个更加深入的知识点 —— 方法引用。通过本小节的学习,你将了解到什么是方法引用方法引用的基础语法,方法引用的使用条件和使用场景方法引用的分类方法引用的使用实例等内容。

 

1. 什么是方法引用

方法引用(Method References
)是一种语法糖,它本质上就是 Lambda
表达式,我们知道Lambda
表达式是函数式接口的实例,所以说方法引用也是函数式接口的实例。

Tips:什么是语法糖?语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin
)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

可以将语法糖理解为汉语中的成语,用更简练的文字表达较复杂的含义。在得到广泛接受的情况下,可以提升交流的效率。

我们来回顾一个之前学过的实例:

实例演示



import java.util.function.Consumer;
public class MethodReferencesDemo1 {
    public static void main(String[] args) {
        Consumer<String> consumer = s -> System.out.println(s);
        consumer.accept("只消费,不返回");
    }
}
12345678910

可查看在线运行效果

运行结果:


只消费,不返回
代码块1

上例是 Java 内置函数式接口中的消费型接口,如果你使用idea
编写代码,System.out.println(s)
这个表达式可以一键替换为方法引用,将鼠标光标放置到语句上,会弹出提示框,再点击Replace lambda with method reference
按钮即可完成一键替换:

替换为方法引用后的实例代码:

实例演示


import java.util.function.Consumer;
public class MethodReferencesDemo1 {
    public static void main(String[] args) {
        Consumer<String> consumer = System.out::println;
        consumer.accept("只消费,不返回");
    }
}
12345678910

可查看在线运行效果

运行结果:


只消费,不返回
代码块1

我们看到System.out.println(s)
这个表达式被替换成了System.out::println
,同样成功执行了代码,看到这里,同学们脑袋上可能全是问号,这是什么语法?我们之前怎么没提过?别着急,我们马上就来讲解语法规则。

 

2. 语法

方法引用使用一对冒号(::
)来引用方法,格式如下:


类或对象 :: 方法名
代码块1

上面实例中方法引用的代码为:


System.out::println
代码块1

其中System.out
就是PrintStream
类的对象,println
就是方法名。

 

3. 使用场景和使用条件

方法引用的使用场景为:当要传递给Lambda
体的操作,已经有实现的方法了,就可以使用方法引用。

方法引用的使用条件为:接口中的抽象方法的形参列表和返回值类型与方法引用的方法形参列表和返回值相同。

 

4. 方法引用的分类

对于方法引用的使用,通常可以分为以下 4 种情况:

  1. 对象 :: 非静态方法
    :对象引用非静态方法,即实例方法;

  2. 类 :: 静态方法
    :类引用静态方法;

  3. 类 :: 非静态方法
    :类引用非静态方法;

  4. 类 :: new
    :构造方法引用。

下面我们根据以上几种情况来看几个实例。

 

4.1 对象引用实例方法

对象引用实例方法,我们已经在上面介绍过了,System.out
就是对象,而println
就是实例方法,这里不再赘述。

 

4.2 类引用静态方法

类引用静态方法,请查看以下实例:

实例演示


import java.util.Comparator;
public class MethodReferencesDemo2 {
    public static void main(String[] args) {
        // 使用 Lambda 表达式
        Comparator<Integer> comparator1 = (t1, t2) -> Integer.compare(t1, t2);
        System.out.println(comparator1.compare(11, 12));
        // 使用方法引用,类 :: 静态方法( compare() 为静态方法)
        Comparator<Integer> comparator2 = Integer::compare;
        System.out.println(comparator2.compare(12, 11));
    }
}
123456789101112131415

可查看在线运行效果

运行结果:


-1
1
代码块12

查看 Java 源码,可观察到compare()
方法是静态方法:

我们再来看一个实例:

实例演示


import java.util.Comparator;
import java.util.function.Function;
public class MethodReferencesDemo3 {
    public static void main(String[] args) {
        // 使用 Lambda 表达式
        Function<Double, Long> function1 = d -> Math.round(d);
        Long apply1 = function1.apply(1.0);
        System.out.println(apply1);
        // 使用方法引用,类 :: 静态方法( round() 为静态方法)
        Function<Double, Long> function2 = Math::round;
        Long apply2 = function2.apply(2.0);
        System.out.println(apply2);
    }
}
123456789101112131415161718

可查看在线运行效果

运行结果:


1
2
代码块12

 

4.3 类引用实例方法

类引用实例方法,比较难以理解,请查看以下实例:

实例演示


import java.util.Comparator;
public class MethodReferencesDemo4 {
    public static void main(String[] args) {
        // 使用 Lambda 表达式
        Comparator<String> comparator1 = (s1, s2) -> s1.compareTo(s2);
        int compare1 = comparator1.compare("Hello", "Java");
        System.out.println(compare1);
        // 使用方法引用,类 :: 实例方法( compareTo() 为实例方法)
        Comparator<String> comparator2 = String::compareTo;
        int compare2 = comparator2.compare("Hello", "Hello");
        System.out.println(compare2);
    }
}
1234567891011121314151617

可查看在线运行效果

运行结果:


-2
0
代码块12

Comparator
接口中的compare(T t1, T t2)
抽象方法,有两个参数,但是String
类下的实例方法compareTo(String anotherString)
只有 1 个参数,为什么这种情况下还能使用方法引用呢?这属于一个特殊情况,当函数式接口中的抽象方法有两个参数时,已实现方法的第 1 个参数作为方法调用者时,也可以使用方法引用。此时,就可以使用类来引用实例方法了(即实例中的String::compareTo
)。

 

4.4 类引用构造方法

类引用构造方法,可以直接使用类名 :: new
,请查看如下实例:

实例演示


import java.util.function.Function;
import java.util.function.Supplier;
public class MethodReferencesDemo5 {
    static class Person {
        private String name;
        public Person() {
            System.out.println("无参数构造方法执行了");
        }
        public Person(String name) {
            System.out.println("单参数构造方法执行了");
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    public static void main(String[] args) {
        // 使用 Lambda 表达式,调用无参构造方法
        Supplier<Person> supplier1 = () -> new Person();
        supplier1.get();
        // 使用方法引用,引用无参构造方法
        Supplier<Person> supplier2 = Person::new;
        supplier2.get();
        // 使用 Lambda 表达式,调用单参构造方法
        Function<String, Person> function1 = name -> new Person(name);
        Person person1 = function1.apply("小斧");
        System.out.println(person1.getName());
        // 使用方法引用,引用单参构造方法
        Function<String, Person> function2 = Person::new;
        Person person2 = function1.apply("小明");
        System.out.println(person2.getName());
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

可查看在线运行效果

运行结果:


无参数构造方法执行了
无参数构造方法执行了
单参数构造方法执行了
小斧
单参数构造方法执行了
小明
代码块123456

在实例中,我们使用了Lambda
表达式和方法引用两种方式,分别调用了静态内部类Person
的无参和单参构造方法。函数式接口中的抽象方法的形参列表与构造方法的形参列表一致,抽象方法的返回值类型就是构造方法所属的类型。

通过本小节的学习,我们知道了方法引用是一个语法糖,它本质上还是Lambda
表达式。方法引用使用一对冒号(::
)来引用方法。要传递给Lambda
体的操作,已经有实现的方法了,就可以使用方法引用;想要使用方法引用,就要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法形参列表和返回值相同。方法引用可能较为抽象,希望同学们课下多加练习。

Java 流式操作

流式操作,是 Java 8
除了Lambda
表达式外的又一重大改变。学习流式操作,就是学习java.util.stream
包下的API
,我们称之为Stream API
,它把真正的函数式编程引入到了 Java 中。

本小节我们将了解到什么是Stream
为什么使用Stream API
流式操作的执行流程如何实例化Stream
Stream
的中间操作
Stream
的终止操作
等内容。

 

1. 什么是 Stream

Stream
是数据渠道,用于操作数据源所生成的元素序列,它可以实现对集合(Collection
)的复杂操作,例如查找、替换、过滤和映射数据等操作。

我们这里说的Stream
不同于java
的输入输出流。另外,Collection 是一种静态的数据结构,存储在内存中,而Stream
是用于计算的,通过CPU
实现计算。注意不要混淆。

TipsStream
自己不会存储数据;Stream
不会改变源对象,而是返回一个新的持有结果的Stream
(不可变性);Stream
操作是延迟执行的(这一点将在后面介绍)。

 

2. 为什么使用 Stream API

我们在实际开发中,项目中的很多数据都来源于关系型数据库(例如 MySQL、Oracle 数据库),我们使用SQL
的条件语句就可以实现对数据的筛选、过滤等等操作;

但也有很多数据来源于非关系型数据库(Redis
MongoDB
等),想要处理这些数据,往往需要在 Java 层面去处理。

使用Stream API
对集合中的数据进行操作,就类似于 SQL 执行的数据库查询。也可以使用Stream API
来执行并行操作。简单来说,Stream API
提供了一种高效且易于使用的处理数据的方式。

 

3. 流式操作的执行流程

流式操作通常分为以下 3 个步骤:

  1. 创建Stream
    对象
    :通过一个数据源(例如集合、数组),获取一个流;

  2. 中间操作:一个中间的链式操作,对数据源的数据进行处理(例如过滤、排序等),直到执行终止操作才执行;

  3. 终止操作:一旦执行终止操作,就执行中间的链式操作,并产生结果。

下图展示了Stream
的执行流程:

接下来我们就按照这 3 个步骤的顺序来展开学习Stream API

 

4. Stream 对象的创建

有 4 种方式来创建Stream
对象。

 

4.1 通过集合创建 Stream

Java 8 的java.util.Collection
接口被扩展,提供了两个获取流的默认方法:

  • default Stream<E> stream()
    :返回一个串行流(顺序流);

  • default Stream<E> parallelStream()
    :返回一个并行流。

实例如下:


// 创建一个集合,并添加几个元素  
List<String> stringList = new ArrayList<>();  
stringList.add("hello");  
stringList.add("world");  
stringList.add("java");  
  
// 通过集合获取串行 stream 对象  
Stream<String> stream = stringList.stream();  
// 通过集合获取并行 stream 对象  
Stream<String> personStream = stringList.parallelStream();
代码块12345678910

串行流并行流的区别是:串行流从集合中取数据是按照集合的顺序的;而并行流是并行操作的,获取到的数据是无序的。

 

4.2 通过数组创建 Stream

Java 8 中的java.util.Arrays
的静态方法stream()
可以获取数组流:

  • static <T> Stream<T> stream(T[] array)
    :返回一个数组流。

此外,stream()
还有几个重载方法,能够处理对应的基本数据类型的数组:

  • public static IntStream stream(int[] array)
    :返回以指定数组作为其源的连续IntStream

  • public static LongStream stream(long[] array)
    :返回以指定数组作为其源的连续LongStream

  • public static DoubleStream stream(double[] array)
    :返回以指定数组作为其源的连续DoubleStream

实例如下:



import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class StreamDemo1 {
    public static void main(String[] args) {
        // 初始化一个整型数组
        int[] arr = new int[]{1,2,3};
        // 通过整型数组,获取整形的 stream 对象
        IntStream stream1 = Arrays.stream(arr);
        // 通过字符串类型的数组,获取泛型类型为 String 的 stream 对象
        String[] stringArr = new String[]{"Hello", "imooc"};
        Stream<String> stream2 = Arrays.stream(stringArr);
    }
}
代码块1234567891011121314151617

 

4.3 通过 Stream 的 of()
方法

可以通过Stream
类下的of()
方法来创建 Stream 对象,实例如下:


import java.util.stream.Stream;
public class StreamDemo1 {
    public static void main(String[] args) {
        // 通过 Stream 类下的 of() 方法,创建 stream 对象、
        Stream<Integer> stream = Stream.of(1, 2, 3);
    }
}
代码块123456789

 

4.4 创建无限流

可以使用Stream
类下的静态方法iterate()
以及generate()
创建无限流:

  • public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    :遍历;

  • public static<T> Stream<T> generate(Supplier<T> s)
    :生成。

创建无限流的这种方式实际使用较少,大家了解一下即可。

 

5. Stream 的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。在终止操作时会一次性全部处理这些中间操作,称为“惰性求值”。下面,我们来学习一下常用的中间操作方法。

 

5.1 筛选与切片

关于筛选和切片中间操作,有下面几个常用方法:

  • filter(Predicate p)
    :接收 Lambda
    ,从流中清除某些元素;

  • distinct()
    :筛选,通过流生成元素的hashCode
    equals()
    方法去除重复元素;

  • limit(long maxSize)
    :截断流,使其元素不超过给定数量;

  • skip(long n)
    :跳过元素,返回一个扔掉了前 n
    个元素的流。若流中元素不足 n
    个,则返回一个空流。与limit(n)
    互补。

我们先来看一个过滤集合元素的实例:

实例演示


import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamDemo2 {
    static class Person {
        private String name;
        private int age;
        public Person() { }
        public Person(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;
        }
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    /**
     * 创建一个Person的集合
     * @return List
     */
    public static List<Person> createPeople() {
        ArrayList<Person> people = new ArrayList<>();
        Person person1 = new Person("小明", 15);
        Person person2 = new Person("小芳", 20);
        Person person3 = new Person("小李", 18);
        Person person4 = new Person("小付", 23);
        Person person5 = new Person("大飞", 22);
        people.add(person1);
        people.add(person2);
        people.add(person3);
        people.add(person4);
        people.add(person5);
        return people;
    }
    public static void main(String[] args) {
        List<Person> people = createPeople();
        // 创建 Stream 对象
        Stream<Person> stream = people.stream();
        // 过滤年龄大于 20 的person
        Stream<Person> personStream = stream.filter(person -> person.getAge() >= 20);
        // 触发终止操作才能执行中间操作,遍历列表中元素并打印
        personStream.forEach(System.out::println);
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071

可查看在线运行效果

运行结果:


Person{name='小芳', age=20}  
Person{name='小付', age=23}  
Person{name='大飞', age=22}
代码块123

实例中,有一个静态内部类Person
以及一个创建Person
的集合的静态方法createPeople()
,在主方法中,我们先调用该静态方法获取到一个Person
列表,然后创建了Stream
对象,再执行中间操作(即调用fliter()
方法),这个方法的参数类型是一个断言型的函数式接口,接口下的抽象方法test()
要求返回boolean
结果,因此我们使用Lambda
表达式,Lambda
体为person.getAge() >= 20
,其返回值就是一个布尔型结果,这样就实现了对年龄大于等于 20 的person
对象的过滤。

由于必须触发终止操作才能执行中间操作,我们又调用了forEach(System.out::println)
,在这里记住它作用是遍历该列表并打印每一个元素即可,我们下面将会讲解。另外,filter()
等这些由于中间操作返回类型为 Stream,所以支持链式操作,我们可以将主方法中最后两行代码合并成一行:


stream.filter(person -> person.getAge() >= 20).forEach(System.out::println);
代码块1

我们再来看一个截断流的使用实例:

实例演示


import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamDemo3 {
    static class Person {
        private String name;
        private int age;
        public Person() { }
        public Person(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;
        }
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    /**
     * 创建一个Person的集合
     * @return List
     */
    public static List<Person> createPeople() {
        ArrayList<Person> people = new ArrayList<>();
        Person person1 = new Person("小明", 15);
        Person person2 = new Person("小芳", 20);
        Person person3 = new Person("小李", 18);
        Person person4 = new Person("小付", 23);
        Person person5 = new Person("大飞", 22);
        people.add(person1);
        people.add(person2);
        people.add(person3);
        people.add(person4);
        people.add(person5);
        return people;
    }
    public static void main(String[] args) {
        List<Person> people = createPeople();
        // 创建 Stream 对象
        Stream<Person> stream = people.stream();
        // 截断流,并调用终止操作打印集合中元素
        stream.limit(2).forEach(System.out::println);
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869

可查看在线运行效果

运行结果:


Person{name='小明', age=15}  
Person{name='小芳', age=20}
代码块12

根据运行结果显示,我们只打印了集合中的前两条数据。

跳过前 2 条数据的代码实例如下:


// 非完整代码
public static void main(String[] args) {
    List<Person> people = createPeople();
    // 创建 Stream 对象
    Stream<Person> stream = people.stream();
    // 跳过前两个元素,并调用终止操作打印集合中元素
    stream.skip(2).forEach(System.out::println);
}
代码块12345678

运行结果:


Person{name='小李', age=18}  
Person{name='小付', age=23}  
Person{name='大飞', age=22}
代码块123

distinct()
方法会根据equals()
hashCode()
方法筛选重复数据,我们在Person
类内部重写这两个方法,并且在createPerson()
方法中,添加几个重复的数据 ,实例如下:

实例演示


import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
public class StreamDemo4 {
    static class Person {
        private String name;
        private int age;
        public Person() { }
        public Person(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;
        }
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age &&
                    Objects.equals(name, person.name);
        }
        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    }
    /**
     * 创建一个Person的集合
     * @return List
     */
    public static List<Person> createPeople() {
        ArrayList<Person> people = new ArrayList<>();
        people.add(new Person("小明", 15));
        people.add(new Person("小芳", 20));
        people.add(new Person("小李", 18));
        people.add(new Person("小付", 23));
        people.add(new Person("小付", 23));
        people.add(new Person("大飞", 22));
        people.add(new Person("大飞", 22));
        people.add(new Person("大飞", 22));
        return people;
    }
    public static void main(String[] args) {
        List<Person> people = createPeople();
        // 创建 Stream 对象
        Stream<Person> stream = people.stream();
        System.out.println("去重前,集合中元素有:");
        stream.forEach(System.out::println);
        System.out.println("去重后,集合中元素有:");
        // 创建一个新流
        Stream<Person> stream1 = people.stream();
        // 截断流,并调用终止操作打印集合中元素
        stream1.distinct().forEach(System.out::println);
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889

可查看在线运行效果

运行结果:


去重前,集合中元素有: 
Person{name='小明', age=15}  
Person{name='小芳', age=20}  
Person{name='小李', age=18}  
Person{name='小付', age=23}  
Person{name='小付', age=23}  
Person{name='大飞', age=22}  
Person{name='大飞', age=22}  
Person{name='大飞', age=22}  
去重后,集合中元素有: 
Person{name='小明', age=15}  
Person{name='小芳', age=20}  
Person{name='小李', age=18}  
Person{name='小付', age=23}  
Person{name='大飞', age=22}
代码块123456789101112131415

 

5.2 映射

关于映射中间操作,有下面几个常用方法:

  • map(Function f)
    :接收一个方法作为参数,该方法会被应用到每个元素上,并将其映射成一个新的元素;

  • mapToDouble(ToDoubleFunction f)
    :接收一个方法作为参数,该方法会被应用到每个元素上,产生一个新的DoubleStream

  • mapToLong(ToLongFunction f)
    :接收一个方法作为参数,该方法会被应用到每个元素上,产生一个新的LongStream

  • flatMap(Function f)
    :接收一个方法作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

请查看如下实例:

实例演示


import java.util.Arrays;
import java.util.List;
public class StreamDemo5 {
    public static void main(String[] args) {
        // 创建一个包含小写字母元素的字符串列表
        List<String> stringList = Arrays.asList("php", "js", "python", "java");
        // 调用 map() 方法,将String下的toUpperCase()方法作为参数,这个方法会被应用到每个元素上,映射成一个新元素,最后打印映射后的元素
        stringList.stream().map(String::toUpperCase).forEach(System.out::println);
    }
}
12345678910111213

可查看在线运行效果

运行结果:


PHP  
JS  
PYTHON  
JAVA
代码块1234

可参考下图,理解映射的过程:

 

5.3 排序

关于排序中间操作,有下面几个常用方法:

  • sorted()
    :产生一个新流,其中按照自然顺序排序;

  • sorted(Comparator com)
    :产生一个新流,其中按照比较器顺序排序。

请查看如下实例:

实例演示


import java.util.Arrays;
import java.util.List;
public class StreamDemo6 {
    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1);
        // 调用sorted()方法自然排序,并打印每个元素
        integers.stream().sorted().forEach(System.out::println);
    }
}
123456789101112

可查看在线运行效果

运行结果:


1  
8  
9  
10  
12  
20
代码块123456

上面实例中,我们调用sorted()
方法对集合元素进行了从小到大的自然排序,那么如果想要实现从大到小排序,任何实现呢?此时就要用到sorted(Comparator com)
方法定制排序,查看如下实例:

实例演示


import java.util.Arrays;
import java.util.List;
public class StreamDemo6 {
    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1);
        // 定制排序
        integers.stream().sorted(
                (i1, i2) -> -Integer.compare(i1, i2)
        ).forEach(System.out::println);
    }
}
1234567891011121314

可查看在线运行效果

运行结果:


20
12
10
9
8
1
代码块123456

实例中,sorted()
方法接收的参数是一个函数式接口Comparator
,因此使用Lambda
表达式创建函数式接口实例即可,Lambda
体调用整型的比较方法,对返回的整型值做一个取反即可。

 

6. Stream 的终止操作

执行终止操作会从流的流水线上生成结果,其结果可以是任何不是流的值,例如List
String
void

在上面实例中,我们一直在使用forEach()
方法来执行流的终止操作,下面我们看看还有哪些其他终止操作。

 

6.1 匹配与查找

关于匹配与查找的终止操作,有下面几个常用方法:

  • allMatch(Predicate p)
    :检查是否匹配所有元素;

  • anyMatch(Predicate p)
    :检查是否至少匹配一个元素;

  • noneMatch(Predicate p)
    :检查是否没有匹配所有元素;

  • findFirst()
    :返回第一个元素;

  • findAny()
    :返回当前流中的任意元素;

  • count()
    :返回流中元素总数;

  • max(Comparator c)
    :返回流中最大值;

  • min(Comparator c)
    :返回流中最小值;

  • forEach(Consumer c)
    :内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代;相反 Stream API
    使用内部迭代)。

如下实例,演示了几个匹配元素相关方法的使用:

实例演示


import java.util.Arrays;
import java.util.List;
public class StreamDemo7 {
    public static void main(String[] args) {
        // 创建一个整型列表
        List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1);
        // 使用 allMatch(Predicate p) 检查是否匹配所有元素,如果匹配,则返回 true;否则返回 false
        boolean b1 = integers.stream().allMatch(integer -> integer > 0);
        if (b1) {
            System.out.println(integers + "列表中所有的元素都大于0");
        } else {
            System.out.println(integers + "列表中不是所有的元素都大于0");
        }
        // 使用 anyMatch(Predicate p) 检查是否至少匹配一个元素
        boolean b2 = integers.stream().anyMatch(integer -> integer >= 20);
        if (b2) {
            System.out.println(integers + "列表中至少存在一个的元素都大于等于20");
        } else {
            System.out.println(integers + "列表中不存在任何一个大于等于20的元素");
        }
        // 使用 noneMath(Predicate p) 检查是否没有匹配所有元素
        boolean b3 = integers.stream().noneMatch(integer -> integer > 100);
        if (b3) {
            System.out.println(integers + "列表中不存在大于100的元素");
        } else {
            System.out.println(integers + "列表中存在大于100的元素");
        }
    }
}
12345678910111213141516171819202122232425262728293031323334

可查看在线运行效果

运行结果:


[10, 12, 9, 8, 20, 1]列表中所有的元素都大于0
[10, 12, 9, 8, 20, 1]列表中至少存在一个的元素都大于等于20
[10, 12, 9, 8, 20, 1]列表中不存在大于100的元素
代码块123

查找元素的相关方法使用实例如下:

实例演示


import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class StreamDemo8 {
    public static void main(String[] args) {
        // 创建一个整型列表
        List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1);
        // 使用 findFirst() 获取当前流中的第一个元素
        Optional<Integer> first = integers.stream().findFirst();
        System.out.println(integers + "列表中第一个元素为:" + first);
        // 使用 findAny() 获取当前流中的任意元素
        Optional<Integer> any = integers.stream().findAny();
        System.out.println("列表中任意元素:" + any);
        // 使用 count() 获取当前流中元素总数
        long count = integers.stream().count();
        System.out.println(integers + "列表中元素总数为" + count);
        // 使用 max(Comparator c) 获取流中最大值
        Optional<Integer> max = integers.stream().max(Integer::compare);
        System.out.println(integers + "列表中最大值为" + max);
        // 使用 min(Comparator c) 获取流中最小值
        Optional<Integer> min = integers.stream().min(Integer::compare);
        System.out.println(integers + "列表中最小值为" + min);
    }
}
1234567891011121314151617181920212223242526272829303132

可查看在线运行效果

运行结果:


[10, 12, 9, 8, 20, 1]列表中第一个元素为:Optional[10]
列表中任意元素:Optional[10]
[10, 12, 9, 8, 20, 1]列表中元素总数为6
[10, 12, 9, 8, 20, 1]列表中最大值为Optional[20]
[10, 12, 9, 8, 20, 1]列表中最小值为Optional[1]
代码块12345

实例中,我们观察到findFirst()
findAny()
max()
等方法的返回值类型为Optional
类型,关于这个Optional
类,我们将在下一小节具体介绍。

 

6.2 归约

关于归约的终止操作,有下面几个常用方法:

  • reduce(T identity, BinaryOperator b)
    :可以将流中的元素反复结合起来,得到一个值。返回 T;

  • reduce(BinaryOperator b)
    :可以将流中的元素反复结合起来,得到一个值,返回 Optional<T>

归约相关方法的使用实例如下:

实例演示


import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class StreamDemo9 {
    public static void main(String[] args) {
        // 创建一个整型列表
        List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1);
        // 使用 reduce(T identity, BinaryOperator b) 计算列表中所有整数和
        Integer sum = integers.stream().reduce(0, Integer::sum);
        System.out.println(sum);
        // 使用 reduce(BinaryOperator b) 计算列表中所有整数和,返回一个 Optional<T>
        Optional<Integer> reduce = integers.stream().reduce(Integer::sum);
        System.out.println(reduce);
    }
}
1234567891011121314151617181920

可查看在线运行效果

运行结果:


60
Optional[60]
代码块12

 

6.3 收集

collect(Collector c)
:将流转换为其他形式。接收一个Collector
接口的实现,用于给Stream
中元素做汇总的方法。

实例如下:

实例演示


import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class StreamDemo10 {
    public static void main(String[] args) {
        // 创建一个整型列表
        List<Integer> integers = Arrays.asList(10, 12, 9, 8, 20, 1, 10);
        Set<Integer> collect = integers.stream().collect(Collectors.toSet());
        System.out.println(collect);
    }
}
123456789101112131415

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:


[1, 20, 8, 9, 10, 12]
代码块1

Collector 接口中的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。java.util.stream.Collectors
类提供了很多静态方法,可以方便地创建常用收集器实例,常用静态方法如下:

  • static List<T> toList()
    :把流中元素收集到List

  • static Set<T> toSet()
    :把流中元素收集到Set

  • static Collection<T> toCollection()
    :把流中元素收集到创建的集合。

通过本小节的学习,我们知道了Stream
不同于java.io
下的输入输出流,它主要用于处理数据。Stream API
可用于处理非关系型数据库中的数据;想要使用流式操作,就要知道创建Stream
对象的几种方式;流式操作可分为创建Stream
对象、中间操作和终止操作三个步骤。多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。执行终止操作会从流的流水线上生成结果,其结果可以是任何不是流的值。

Optional 类

上一小节,我们接触到了Optional
类,但没有详细展开介绍,Optional
类也是 Java 8 新加入的类。本小节我们就来学习一下这个类,你将了解到Optional
类的解决了什么问题,如何创建Optioanl
类的对象,它又有哪些常用方法,如何在实际开发中应用Optional
类等内容。

 

1. Optional 类概述

空指针异常(NullPointerExceptions
)是 Java 最常见的异常之一,一直以来都困扰着 Java 程序员。一方面,程序员不得不在代码中写很多null
的检查逻辑,让代码看起来非常臃肿;另一方面,由于其属于运行时异常,是非常难以预判的。

为了预防空指针异常,Google
Guava
项目率先引入了Optional
类,通过使用检查空值的方式来防止代码污染,受到Guava
项目的启发,随后在Java 8
中也引入了Optional
类。

Optional 类位于java.util
包下,是一个可以为 null
容器对象,如果值存在则isPresent()
方法会返回 true
,调用 get()
方法会返回该对象,可以有效避免空指针异常。下面我们来学习如何实例化这个类,以及这个类下提供了哪些常用方法。

 

2. 创建 Optional 对象

查看 java.util.Optional
类源码,可以发现其构造方法是私有的,因此不能通过new
关键字来实例化:

我们可以通过如下几种方法,来创建Optional 对象:

  • Optional.of(T t)
    :创建一个 Optional 对象,参数 t
    必须非空;

  • Optional.empty()
    :创建一个空的Optional
    实例;

  • Optional.ofNullable(T t)
    :创建一个Optional
    对象,参数t
    可以为 null

实例如下:

实例演示



import java.util.Optional;
public class OptionalDemo1 {
    public static void main(String[] args) {
        // 创建一个 StringBuilder 对象
        StringBuilder string = new StringBuilder("我是一个字符串");
        
        // 使用 Optional.of(T t) 方法,创建 Optional 对象,注意 T 不能为空:
        Optional<StringBuilder> stringBuilderOptional = Optional.of(string);
        System.out.println(stringBuilderOptional);
        // 使用 Optional.empty() 方法,创建一个空的 Optional 对象:
        Optional<Object> empty = Optional.empty();
        System.out.println(empty);
        // 使用 Optional.ofNullable(T t) 方法,创建 Optional 对象,注意 t 允许为空:
        stringBuilderOptional = null;
        Optional<Optional<StringBuilder>> stringBuilderOptional1 = Optional.ofNullable(stringBuilderOptional);
        System.out.println(stringBuilderOptional1);
    }
}
1234567891011121314151617181920212223

可查看在线运行效果

运行结果:


Optional[我是一个字符串]
Optional.empty
Optional.empty
代码块123

 

3. 常用方法

Optional<T>
类提供了如下常用方法:

  • booean isPresent()
    :判断是否包换对象;

  • void ifPresent(Consumer<? super T> consumer)
    :如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传递给它;

  • T get()
    :如果调用对象包含值,返回该值,否则抛出异常;

  • T orElse(T other)
    :如果有值则将其返回,否则返回指定的other
    对象;

  • T orElseGet(Supplier<? extends T other>)
    :如果有值则将其返回,否则返回由Supplier
    接口实现提供的对象;

  • T orElseThrow(Supplier<? extends X> exceptionSupplier)
    :如果有值则将其返回,否则抛出由Supplier
    接口实现提供的异常。

知道了如何创建Optional
对象和常用方法,我们下面结合具体实例来看一下,Optional
类是如何避免空指针异常的。

请查看如下实例,其在运行时会发生空指针异常:

实例演示


import java.util.Optional;
public class OptionalDemo2 {
    static class Category {
        private String name;
        public Category(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Category{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    static class Goods {
        private String name;
        private Category category;
        public Goods() {
        }
        public Goods(String name, Category category) {
            this.name = name;
            this.category = category;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Category getCategory() {
            return category;
        }
        public void setCategory(Category category) {
            this.category = category;
        }
        @Override
        public String toString() {
            return "Good{" +
                    "name='" + name + '\'' +
                    ", category=" + category +
                    '}';
        }
    }
    /**
     * 获取商品的分类名称
     * @param goods 商品
     * @return 分类名称
     */
    static String getGoodsCategoryName(Goods goods) {
        return goods.getCategory().getName();
    }
    public static void main(String[] args) {
        // 实例化一个商品类
        Goods goods = new Goods();
        // 获取商品的分类名称
        String categoryName = getGoodsCategoryName(goods);
        System.out.println(categoryName);
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283

可查看在线运行效果

运行结果:


Exception in thread "main" java.lang.NullPointerException
    at OptionalDemo2.getGoodsCategoryName(OptionalDemo2.java:73)
    at OptionalDemo2.main(OptionalDemo2.java:80)
代码块123

实例中,由于在实例化Goods
类时,我们没有给其下面的Category
类型的属性category
赋值,它就为 null
,在运行时, null.getName()
就会抛出空指针异常。同理,如果goods
实例为null
,那么null.getCategory()
也会抛出空指针异常。

在没有使用Optional
类的情况下,想要优化代码,就不得不改写getGoodsCategoryName()
方法:


static String getGoodsCategoryName(Goods goods) {
    if (goods != null) {
        Category category = goods.getCategory();
        if (category != null) {
            return category.getName();
        }
    }
    return "该商品无分类";
}
代码块123456789

这也就是我们上面说的null
检查逻辑代码,此处有两层if
嵌套,如果有更深层次的级联属性,就要嵌套更多的层级。

下面我们将Optional
类引入实例代码:

实例演示


import java.util.Optional;
public class OptionalDemo3 {
    static class Category {
        private String name;
        public Category(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Category{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    static class Goods {
        private String name;
        private Category category;
        public Goods() {
        }
        public Goods(String name, Category category) {
            this.name = name;
            this.category = category;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Category getCategory() {
            return category;
        }
        public void setCategory(Category category) {
            this.category = category;
        }
        @Override
        public String toString() {
            return "Good{" +
                    "name='" + name + '\'' +
                    ", category=" + category +
                    '}';
        }
    }
    /**
     * 获取商品的分类名称(使用 Optional 类包装)
     * @param goods 商品
     * @return 分类名称
     */
    static String getGoodsCategoryName(Goods goods) {
        // 将商品实例包装入 Optional 类,创建 Optional<Goods> 对象
        Optional<Goods> goodsOptional = Optional.ofNullable(goods);
        Goods goods1 = goodsOptional.orElse(new Goods("默认商品", new Category("默认分类")));
        // 此时 goods1 一定是非空,不会产生空指针异常
        Category category = goods1.getCategory();
        // 将分类实例包装入 Optional 类,创建 Optional<Category> 对象
        Optional<Category> categoryOptional = Optional.ofNullable(category);
        Category category1 = categoryOptional.orElse(new Category("默认分类"));
        // 此时 category1 一定是非空,不会产生空指针异常
        return category1.getName();
    }
    public static void main(String[] args) {
        // 实例化一个商品类
        Goods goods = null;
        // 获取商品的分类名称
        String categoryName = getGoodsCategoryName(goods);
        System.out.println(categoryName);
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293

可查看在线运行效果

运行结果:


默认分类
代码块1

实例中,我们使用Optional
类的 ofNullable(T t)
方法分别包装了goods
对象及其级联属性category
对象,允许对象为空,然后又调用了其ofElse(T t)
方法保证了对象一定非空。这样,空指针异常就被我们优雅地规避掉了。

 

4. 对于空指针异常的改进

Java 14 对于空指针异常有了一些改进,它提供了更明确异常堆栈打印信息,JVM 将精确地确定那个变量是null
,不过空指针异常依然无法避免。明确的异常堆栈信息,能够帮助开发者快速定位错误发生的位置。

通过本小节的学习,我们知道了 Optional
类主要用于应对 Java 中的空指针异常,它是一个可以为 null
的容器对象,我们可以通过Optional
类下的几个静态方法来创建对象。另外,我们也结合实例介绍了如何使用Optional
类来规避空指针异常,实例中还有很多其他没用到的 API,希望大家可以自己研习。

实战 - 需求分析

经过前面这么久的 Java 学习,相信大家也积累了不少知识,我们已经对 Java 语言有了系统的了解,接下来最后的这几节内容,我们将一起实现一个实战项目 —— 商品管理系统。本小节我们就来对这个商品管理系统进行需求分析,你将了解到什么是需求分析需求分析的重要性等。分析好需求之后,你将知道想要实现这个程序,需要什么前置知识

 

1. 需求分析碎碎念

需求分析也称为软件需求分析、系统需求分析或需求分析工程等,是开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体要求,将用户非形式的需求表述转化为完整的需求定义,从而确定系统必须做什么的过程。

第一次听到需求分析这个词,还是在我上大学的时候,当时在某公司的信息部门做实习生,自己什么也不懂,只是一心想要从事软件行业,对于如何编程,开发一个软件的流程完全没有概念。正巧有一个内部项目需要迭代,我们的项目经理和我说,你去做下需求分析吧,既然想要从事软件行业,软件研发的第一步也是最重要的一个环节就要学会做需求分析。项目经理的话我一直记忆犹新。

试想开发一个软件,我们没有做需求分析,就不知道从何下手;如果没有做好需求分析,开发出来的软件和用户的预期不符,就不得不返工重做。

下面我们就进入正题,做这个商品管理系统的需求分析。

 

2. 商品管理系统模块拆分

假设有一个小卖铺的老板找到你,提出了如下需求:

  • 自己平时用手工记录商品基本信息、库存等信息,太麻烦了,希望通过软件实现信息化管理;

  • 能够查看商品详情;

  • 能够对商品进行归类。

根据用户提出的需求,可知客户对系统要求不高,需求也比较明了,一般实际更复杂的项目,需要我们反复和客户进行沟通以核对需求,接下来我们开始根据功能拆分模块。

 

2.1 鉴权模块

该系统是商店的内部系统,因此不能每个人都能使用该系统,需要提供用户鉴权功能,我们使用最简单的用户名和密码进行权限鉴定。权限鉴定通过后,方可使用其他功能。

 

2.2 商品模块

商品模块是该系统的最核心模块,该模块主要对商品进行管理,包括添加商品、编辑商品、查看商品详情、删除商品、搜索商品、更新库存信息等操作,主要为商品的 CRUD
(增删改查) 操作。

 

2.3 分类模块

分类模块主要对商品分类进行管理,同样包含了对商品分类的CRUD
操作,关于实现细节此处我们不过多考虑。

该系统功能模块的思维导图如下:

 

3. 技术选型及所需基础

刚刚我们已经针对客户提出的需求,做了功能模块的拆分,对于该系统也有了一个初步的轮廓。我们就可以决定技术选型了,开发语言我们当然使用的是 Java 语言;数据库采用业界最常用的 MySQL
数据库。因此,想要独立完成此系统的开发,需要你有以下前置基础:

  • Java 语言基础:我们使用 Java 语言开发此系统,因此需要你有扎实的语法基础;

  • 熟悉文本扫描器类的使用:该系统使用终端界面来实现人机交互,因此需要你了解如何使用Scanner
    类,如果不够熟悉,可前往Java Scanner 类小节去回顾;

  • 熟悉 JDBC
    的使用:关于JDBC
    ,我们已经在数据库编程这一小节详细介绍;

  • Maven 基础:该实战项目使用 Maven
    构建工具进行构建,如果不够熟悉,推荐先去学习下另外一个夜猫编程的 wiki
    教程 —— Maven 入门教程

  • MySQL
    基础:数据库使用 MySQL
    ,因此需要你了解如何安装配置MySQL
    数据库,也要掌握基本的SQL
    语句,如果你还不够熟悉,推荐先去学习下另外两个夜猫编程wiki
    教程:


  • MySQL

    基础:数据库使用


    MySQL

    ,因此需要你了解如何安装配置


    MySQL

    数据库,也要掌握基本的


    SQL

    语句,如果你还不够熟悉,推荐先去学习下另外两个夜猫编程


    wiki

本小节,我们阐述了需求分析的重要性,也对我们将要着手开发的系统进行了需求分析,根据需求将系统拆分成了几大功能模块,最后,我们也说明了该项目的所需基础,如果你还未满足,我也给出了一些学习链接,可以去到对应的超链接查漏补缺。另外,对于基础基本满足的同学,我也推荐大家先去自己实现下这个系统,过程中遇到问题自己去查找资料解决,这样你的编码能力、问题解决能力才能真正得到锻炼。

实战 - 数据库设计

经过上一小节的需求分析,我们将系统分为了鉴权模块、商品模块和分类模块,本节中,我们将围绕功能模块,进行数据库设计。你将学习到实际开发中的一些数据库设计技巧。请确保在你的开发环境下,已经准备好了一个数据库。MySQL

 

1. 创建库

imooc_goods_cms
cms
Content Management System

德, 1000.


CREATE DATABASE `imooc_goods_cms` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
代码块1

 

2. 创建表

 

2.1 通用字段

有一定数据库设计经验的同学都知道,一个数据库中的数据表都会有一些通用字段。关于通用字段有哪些、如何命名以及如何选定字段类型都有一定的套路,不仅受个人开发习惯的影响,也受到团队开发规范的约束。

该项目,所有的数据表都包含以下字段:

字段名称字段类型字段长度向努尔默认值注释
idint0表主键,自增长
create_time时间戳-CURRENT_TIMESTAMP创建时间
update_time时间戳-CURRENT_TIMESTAMP更新时间
delete_time时间戳-删除时间

yemao

 

2.2 用户表

由于该系统鉴权模块使用用户名和密码进行鉴权,需要设计一个用户表,用户表包含如下字段(已省略通用字段):imooc_user

字段名称字段类型字段长度向努尔默认值注释
昵称瓦尔查尔50空字符串昵称
用户名瓦尔查尔50用户名
密码32密码

提示password
char
MD5
32

10月 1 日,SQL:MySQL
imooc_goods_cms



-- ----------------------------
-- Table structure for imooc_user
-- ----------------------------
DROP TABLE IF EXISTS `imooc_user`;
CREATE TABLE `imooc_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT "昵称",
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT "用户名",
  `password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT "密码",
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `delete_time` timestamp(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of imooc_user
-- ----------------------------
INSERT INTO `imooc_user` VALUES (1, '小斧', 'admin', '123456', '2020-07-20 16:53:19', '2020-07-20 16:53:19', NULL);
代码块12345678910111213141516171819

 

2.3 商品表

商品模块需要一个商品表,商品表包含如下字段(已省略通用字段):imooc_goods

字段名称字段类型字段长度允许为 NULL默认值注释
namevarchar100商品名
descriptionvarchar255空字符串简介
category_idint110分类id
pricedicimal10, 2价格
stockint110库存

链接并选择数据库,执行以下 SQL:MySQL
imooc_goods_cms


-- ----------------------------
-- Table structure for imooc_goods
-- ----------------------------
DROP TABLE IF EXISTS `imooc_goods`;
CREATE TABLE `imooc_goods`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  `category_id` int(11) NULL DEFAULT 0,
  `price` decimal(10, 2) NOT NULL,
  `stock` int(11) NULL DEFAULT 0,
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `delete_time` timestamp(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of imooc_goods
-- ----------------------------
INSERT INTO `imooc_goods` VALUES (1, '测试商品1', '', 0, 12.30, 3, '2020-07-20 16:53:19', '2020-07-20 16:53:19', NULL);
INSERT INTO `imooc_goods` VALUES (2, '测试商品2', '', 0, 33.20, 10, '2020-07-20 17:17:53', '2020-07-20 17:17:53', NULL);
INSERT INTO `imooc_goods` VALUES (3, '测试商品3', '', 0, 20.00, 50, '2020-07-20 17:18:09', '2020-07-20 17:18:09', NULL);
代码块1234567891011121314151617181920212223

 

2.4 分类表

分类模块需要一个商品表,分类表包含如下字段(已省略通用字段):imooc_category

字段名称字段类型字段长度允许为 NULL默认值注释
namevarchar100商品名
descriptionvarchar255空字符串简介

链接并选择数据库,执行以下 SQL:MySQL
imooc_goods_cms


-- ----------------------------
-- Table structure for imooc_category
-- ----------------------------
DROP TABLE IF EXISTS `imooc_category`;
CREATE TABLE `imooc_category`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
代码块12345678910

至此,我们的数据库和表已经建立完成。

本小节,我们一起创建了数据库和表,表结构比较简单,大家可以去我的代码仓库找到文件,并直接在你的数据库中执行,下一小节,我们将着手业务代码的实现。SQL

实战 - 业务实现 1

上一小节我们完成了数据库的设计和创建,也向数据表中插入了一些初始数据,本小节我们将开始具体业务代码的实现,如果大家还没有完成上一小节的任务,请务必先完成再来学习本节内容。

 

1. 准备工作

在开始正式编码之前,我们要做一些准备工作,主要是环境的搭建和工具类的引入。

 

1.1 创建 Maven 工程

打开 idea
,点击Create new Project
按钮:

在左侧栏选择Maven
Project SDK
选择14
,勾选Create from archetype
复选框,再选择maven-archetype-quickstart
,表示创建一个简单 Java 应用,点击next
按钮:

输入项目名称goods
,将项目路径设置为本地桌面,GroupId
可根据实际情况自定义,此处我设置为com.colorful
,其余输入框无需修改,采用默认即可,设置完成后,点击next
按钮:

这一步来到Maven
配置,idea
自带了Maven
,我们使用默认的即可,直接点击Finish
按钮完成项目创建:

此时,Maven
会进行一些初始化配置,右下角对话框选择Enable Auto-import
按钮,表示允许自动导入依赖:

稍等片刻,待看到左侧项目的目录结构已经生成好了,及表示已完成项目的初始化工作:

 

1.2 引入 MySQL 驱动

接下来引入mysql-connector-java
驱动,由于我本地安装的MySQL
版本为8.0.21
,因此mysql-connector-java
的版本号也选择8.0.21
,大家根据自己实际情况选择对应版本。

打开pom.xml
文件,在<dependencies></dependencies>
节点内插入如下xml


<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
代码块123456

由于我们已经配置了允许自动导入依赖,稍等片刻,mysql-connector-java 8.0.21
就会被成功导入。可在idea
右侧点击Maven
按钮查看项目的依赖关系:

 

1.3 引入 JDBC 工具类

JDBC 相关操作是本项目的最常用的操作,我封装了一个 JDBC 的工具类,主要通过 Java 的 JDBC API 去访问数据库,提供了加载配置、注册驱动、获得资源以及释放资源等接口。

大家可以到我的 Github
仓库下载这个 JDBCUtil
类;也可以直接复制下面的代码:



package com.colorful.util;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
/**
 * @author colorful@TaleLin
 */
public class JDBCUtil {
    private static final String driverClass;
    private static final String url;
    private static final String username;
    private static final String password;
    static {
        // 加载属性文件并解析
        Properties props = new Properties();
        // 使用类的加载器的方式进行获取配置
        InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");
        try {
            assert inputStream != null;
            props.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        driverClass = props.getProperty("driverClass");
        url = props.getProperty("url");
        username = props.getProperty("username");
        password = props.getProperty("password");
    }
    /**
     * 注册驱动
     */
    public static void loadDriver() throws ClassNotFoundException{
        Class.forName(driverClass);
    }
    /**
     * 获得连接
     */
    public static Connection getConnection() throws Exception{
        loadDriver();
        return DriverManager.getConnection(url, username, password);
    }
    /**
     * 资源释放
     */
    public static void release(PreparedStatement statement, Connection connection){
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            connection = null;
        }
    }
    /**
     * 释放资源 重载方法
     */
    public static void release(ResultSet rs, PreparedStatement stmt, Connection conn){
        if(rs!= null){
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if(stmt != null){
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            stmt = null;
        }
        if(conn != null){
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103

我本地将这个类放在了 com.colorful.util
包下,大家可根据自身情况随意放置。另外,由于该类在静态代码块中加载了配置文件jdbc.properties
,需要在resource
下面新建一个 jdbc.properties
文件,并写入一下内容:


driverClass=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///yemaobiancheng?serverTimezone=Asia/Shanghai&characterEncoding=UTF8
username=root
password=123456
代码块1234

我将数据放到了本地系统中,并且启动端口是默认 3306,大家根据自己的MySQL
实际配置自行修改。

 

1.4 测试代码

为了测试我们的数据库配置以及 JDBCUtil
类是否成功引入,现在到 test 目录下,新建一个 JDBCTest
类:


package com.colorful;
import com.colorful.util.JDBCUtil;
import org.junit.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
public class JDBCTest {
    @Test
    public void testJDBC() {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            // 获得链接
            connection = JDBCUtil.getConnection();
            // 编写 SQL 语句
            String sql = "SELECT * FROM `imooc_user` where `id` = ?";
            // 预编译 SQL
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数
            preparedStatement.setInt(1, 1);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                int id = resultSet.getInt("id");
                String nickname = resultSet.getString("nickname");
                Timestamp createTime = resultSet.getTimestamp("create_time");
                System.out.println("id=" + id);
                System.out.println("nickname=" + nickname);
                System.out.println("createTime=" + createTime);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            JDBCUtil.release(resultSet, preparedStatement, connection);
        }
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435363738394041424344

如果配置成功,运行单元测试,将得到如下运行结果:


id=1
nickname=小斧
createTime=2020-07-20 16:53:19.0
代码块123

下面为运行截图:

 

2. 系统架构

本商品管理系统的包结构如下:


src
├── main       
│   ├── java    # 源码目录
│   │   └── com
│   │       └── colorful
│   │           ├── App.java    # 入口文件
│   │           ├── dao         # 数据访问对象(Data Access Object,提供数据库操作的一些方法)
│   │           ├── model       # 实体类(类字段和数据表字段一一对应)
│   │           ├── service     # 服务层(提供业务逻辑层服务)
│   │           └── util        # 一些帮助类
│   └── resources
│       ├── imooc_goods_cms.sql # 建表的 SQL 文件
│       └── jdbc.properties     # jdbc 配置文件
└── test       # 单元测试目录
    └── java
        └── com
            └── colorful
                ├── AppTest.java
                └── JDBCTest.java
代码块12345678910111213141516171819

大家可以提前熟悉一下本项目的项目结构,下面我们会一一讲解。

 

3. 实体类

实体类的作用是存储数据并提供对这些数据的访问。在我们这个项目中,实体类统一被放到了model
包下,通常情况下,实体类中的属性与我们的数据表字段一一对应。当我们编写这些实体类的时候,建议对照着数据表的字段以防疏漏。

 

3.1 BaseModel

在我们数据表中,有几个公共的字段,可以提取出一个实体类的父类 BaseModel
,并提供 getter
setter
,源码如下:


package com.colorful.model;
import java.sql.Timestamp;
public class BaseModel {
    private Integer id;
    private Timestamp createTime;
    private Timestamp updateTime;
    private Timestamp deleteTime;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public Timestamp getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Timestamp createTime) {
        this.createTime = createTime;
    }
    public Timestamp getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Timestamp updateTime) {
        this.updateTime = updateTime;
    }
    public Timestamp getDeleteTime() {
        return deleteTime;
    }
    public void setDeleteTime(Timestamp deleteTime) {
        this.deleteTime = deleteTime;
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

值得注意的是,Timestamp
java.sql
下的类。

 

3.2 实体类编写

接下来,再在model
包下新建 3 个类:User
Goods
Category
,并提供getter
setter
。如下是每个类的代码:


package com.colorful.model;
public class User extends BaseModel {
    private String userName;
    private String nickName;
    private String password;
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getNickName() {
        return nickName;
    }
    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435
package com.colorful.model;
public class Goods extends BaseModel {
    private String name;
    private String description;
    private Integer categoryId;
    private Double price;
    private Integer stock;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Integer getCategoryId() {
        return categoryId;
    }
    public void setCategoryId(Integer categoryId) {
        this.categoryId = categoryId;
    }
    public Double getPrice() {
        return price;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
    public Integer getStock() {
        return stock;
    }
    public void setStock(Integer stock) {
        this.stock = stock;
    }
}
代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
package com.colorful.model;
public class Category extends BaseModel {
    private String name;
    private String description;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}
代码块12345678910111213141516171819202122232425

 

4. 实现用户鉴权

 

4.1 登录方式

想要使用系统进行商品管理,第一步要做的就是登录。

我们的系统使用用户名和密码进行登录校验,上一小节我们已经建立了imooc_user
表,并向表中插入了一个用户 admin
,其密码为 123456
。显然,通过如下SQL
就可以查询到该用户:


SELET * FROM `imooc_user` WHERE `username` = 'admin' AND password = '123456';
代码块1

如果查询到这个用户,就表示用户名密码通过校验,用户可执行后续操作,如果没有查到,就要提示用户重新输入账号和密码。

 

4.2 数据访问对象

我们先不管用户是如何输入账号密码的,接下来要编写的业务代码就是根据用户名和密码去查询用户。那么涉及到数据库查询的代码应该放到哪里呢?参考上面的系统架构图,DAO
是数据访问对象,我们可以在dao
包下面新建一个UserDAO
,并写入如下代码:


package com.colorful.dao;
import com.colorful.model.User;
import com.colorful.util.JDBCUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class UserDAO {
    public User selectByUserNameAndPassword(String username, String password) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        User user = new User();
        try {
            // 获得链接
            connection = JDBCUtil.getConnection();
            // 编写 SQL 语句
            String sql = "SELECT * FROM `imooc_user` where `username` = ? AND `password` = ? AND `delete_time` is null ";
            // 预编译 SQL
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数
            preparedStatement.setString(1, username);
            preparedStatement.setString(2, password);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                user.setId(resultSet.getInt("id"));
                String nickname = resultSet.getString("nickname");
                if (nickname.equals("")) {
                    nickname = "匿名";
                }
                user.setNickName(nickname);
                user.setUserName(resultSet.getString("username"));
            } else {
                user = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            JDBCUtil.release(resultSet, preparedStatement, connection);
        }
        return user;
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142434445464748

UserDAO
类下面有一个 selectByUserNameAndPassword()
方法, 接收两个参数 username
password
,返回值类型是实体类 User
,如果没有查询到,返回的是一个 null

完成了 UserDAO
的编写,我们需要到服务层 service
包下,新建一个 UserService
,并写入如下代码:


package com.colorful.service;
import com.colorful.dao.UserDAO;
import com.colorful.model.User;
public class UserService {
    private final UserDAO userDAO = new UserDAO();
    // 登陆
    public User login(String username, String password) {
        return userDAO.selectByUserNameAndPassword(username, password);
    }
    
}
代码块123456789101112131415

到这里大家可能有些疑问,这个类下面的login()
方法,直接调用了我们刚刚编写的 DAO
下面的 selectByUserNameAndPassword()
方法,为什么还要嵌套这么一层么?这不是多此一举么?

要讨论 service
层的封装是不是过度设计,就要充分理解设计服务层的概念和意义,服务层主要是对业务逻辑的封装,对于更为复杂的项目,用户登录会有更多的方式,因此在服务层,会封装更多的业务逻辑。如果没有服务层,这些复杂的逻辑不得不都写在数据访问层,显然这是不合理的。我们现在这个项目没有使用任何框架,等到后面大家学习了Spring
这种框架,一定会对这样的分层的好处有所体会。

 

4.3 使用 Scanner 类与用户交互

完成了上面一系列的封装,就剩下我们和用户的交互了,本项目中,我们使用 Scanner
类来接收用户的输入,并使用print()
方法向屏幕输出。

打开 App.java
入口文件,创建UserService
实例,编写一个主流程方法 run()
,并在入口方法 main()
中调用该方法:


package com.colorful;
import com.colorful.model.User;
import com.colorful.service.UserService;
import java.util.Scanner;
/**
 * @author colorful@TaleLin
 * Imooc Goods
 */
public class App {
    private static final UserService userService = new UserService();
    
    /**
     * 主流程方法
     */
    public static void run() {
        User user = null;
        System.out.println("欢迎使用商品管理系统,请输入用户名和密码:");
        do {
            Scanner scanner = new Scanner(System.in);
            // 登录
            System.out.println("用户名:");
            String username = scanner.nextLine();
            System.out.println("密码:");
            String password = scanner.nextLine();
            user = userService.login(username, password);
            if (user == null) {
                System.out.println("用户名密码校验失败,请重新输入!");
            }
        } while (user == null);
        System.out.println("欢迎您!" + user.getNickName());
        // TODO 登录成功,编写后续逻辑
    }
    public static void main( String[] args )
    {
        run();
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142

run()
方法中有一个 do ... while
循环,循环的条件是 user
对象为 null

我们知道,do... while
循环会首先执行 do
中的循环体,循环体中创建了一个 Scanner
类的实例,获取到用户的输入后,我们会调用用户服务层的login()
方法,该方法返回实体类对象User
,如果其为 null
表示用户名密码校验失败,需要用户重新输入, user == null
,满足循环的条件,会一直执行循环体中的代码。直到循环体中的 user
不为 null
(也就是用户登录校验成功后)才终止循环。

下面运行App.java
main()
方法,如下为登录失败的截图:

如果用户名密码检验错误,就要反复输入用户名密码重新登录。

如下为登录成功的截图:

在本小节,我们成功搭建了项目工程,通过实现一个用户鉴权模块,介绍了整体的系统架构。我们在编写实体类的同时,复习了面向对象的继承性;在数据访问层,也复习了 JDBC API
的使用;在编写程序入口文件的同时,也复习了 Scanner
类的使用和循环的使用。

关于系统鉴权,这里还有一个待优化的地方,大家下去之后可以思考一下,在下一小节的开头,我将带领大家一起来优化。下一小节也将主要讲解最后剩余的商品模块和分类模块的实现,也会复习到很多其他方面的基础知识。

实战 - 业务实现 1

上一小节我们完成了数据库的设计和创建,也向数据表中插入了一些初始数据,本小节我们将开始具体业务代码的实现,如果大家还没有完成上一小节的任务,请务必先完成再来学习本节内容。

 

1. 准备工作

在开始正式编码之前,我们要做一些准备工作,主要是环境的搭建和工具类的引入。

 

1.1 创建 Maven 工程

打开 idea
,点击Create new Project
按钮:

在左侧栏选择Maven
Project SDK
选择14
,勾选Create from archetype
复选框,再选择maven-archetype-quickstart
,表示创建一个简单 Java 应用,点击next
按钮:

输入项目名称goods
,将项目路径设置为本地桌面,GroupId
可根据实际情况自定义,此处我设置为com.colorful
,其余输入框无需修改,采用默认即可,设置完成后,点击next
按钮:

这一步来到Maven
配置,idea
自带了Maven
,我们使用默认的即可,直接点击Finish
按钮完成项目创建:

此时,Maven
会进行一些初始化配置,右下角对话框选择Enable Auto-import
按钮,表示允许自动导入依赖:

稍等片刻,待看到左侧项目的目录结构已经生成好了,及表示已完成项目的初始化工作:

 

1.2 引入 MySQL 驱动

接下来引入mysql-connector-java
驱动,由于我本地安装的MySQL
版本为8.0.21
,因此mysql-connector-java
的版本号也选择8.0.21
,大家根据自己实际情况选择对应版本。

打开pom.xml
文件,在<dependencies></dependencies>
节点内插入如下xml


<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
代码块123456

由于我们已经配置了允许自动导入依赖,稍等片刻,mysql-connector-java 8.0.21
就会被成功导入。可在idea
右侧点击Maven
按钮查看项目的依赖关系:

 

1.3 引入 JDBC 工具类

JDBC 相关操作是本项目的最常用的操作,我封装了一个 JDBC 的工具类,主要通过 Java 的 JDBC API 去访问数据库,提供了加载配置、注册驱动、获得资源以及释放资源等接口。

大家可以到我的 Github
仓库下载这个 JDBCUtil
类;也可以直接复制下面的代码:



package com.colorful.util;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
/**
 * @author colorful@TaleLin
 */
public class JDBCUtil {
    private static final String driverClass;
    private static final String url;
    private static final String username;
    private static final String password;
    static {
        // 加载属性文件并解析
        Properties props = new Properties();
        // 使用类的加载器的方式进行获取配置
        InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");
        try {
            assert inputStream != null;
            props.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        driverClass = props.getProperty("driverClass");
        url = props.getProperty("url");
        username = props.getProperty("username");
        password = props.getProperty("password");
    }
    /**
     * 注册驱动
     */
    public static void loadDriver() throws ClassNotFoundException{
        Class.forName(driverClass);
    }
    /**
     * 获得连接
     */
    public static Connection getConnection() throws Exception{
        loadDriver();
        return DriverManager.getConnection(url, username, password);
    }
    /**
     * 资源释放
     */
    public static void release(PreparedStatement statement, Connection connection){
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            connection = null;
        }
    }
    /**
     * 释放资源 重载方法
     */
    public static void release(ResultSet rs, PreparedStatement stmt, Connection conn){
        if(rs!= null){
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if(stmt != null){
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            stmt = null;
        }
        if(conn != null){
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103

我本地将这个类放在了 com.colorful.util
包下,大家可根据自身情况随意放置。另外,由于该类在静态代码块中加载了配置文件jdbc.properties
,需要在resource
下面新建一个 jdbc.properties
文件,并写入一下内容:


driverClass=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///imooc_goods_cms?serverTimezone=Asia/Shanghai&characterEncoding=UTF8
username=root
password=123456
代码块1234

我将数据放到了本地系统中,并且启动端口是默认 3306,大家根据自己的MySQL
实际配置自行修改。

 

1.4 测试代码

为了测试我们的数据库配置以及 JDBCUtil
类是否成功引入,现在到 test 目录下,新建一个 JDBCTest
类:


package com.colorful;
import com.colorful.util.JDBCUtil;
import org.junit.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
public class JDBCTest {
    @Test
    public void testJDBC() {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            // 获得链接
            connection = JDBCUtil.getConnection();
            // 编写 SQL 语句
            String sql = "SELECT * FROM `imooc_user` where `id` = ?";
            // 预编译 SQL
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数
            preparedStatement.setInt(1, 1);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                int id = resultSet.getInt("id");
                String nickname = resultSet.getString("nickname");
                Timestamp createTime = resultSet.getTimestamp("create_time");
                System.out.println("id=" + id);
                System.out.println("nickname=" + nickname);
                System.out.println("createTime=" + createTime);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            JDBCUtil.release(resultSet, preparedStatement, connection);
        }
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435363738394041424344

如果配置成功,运行单元测试,将得到如下运行结果:


id=1
nickname=小斧
createTime=2020-07-20 16:53:19.0
代码块123

下面为运行截图:

 

2. 系统架构

本商品管理系统的包结构如下:


src
├── main       
│   ├── java    # 源码目录
│   │   └── com
│   │       └── colorful
│   │           ├── App.java    # 入口文件
│   │           ├── dao         # 数据访问对象(Data Access Object,提供数据库操作的一些方法)
│   │           ├── model       # 实体类(类字段和数据表字段一一对应)
│   │           ├── service     # 服务层(提供业务逻辑层服务)
│   │           └── util        # 一些帮助类
│   └── resources
│       ├── imooc_goods_cms.sql # 建表的 SQL 文件
│       └── jdbc.properties     # jdbc 配置文件
└── test       # 单元测试目录
    └── java
        └── com
            └── colorful
                ├── AppTest.java
                └── JDBCTest.java
代码块12345678910111213141516171819

大家可以提前熟悉一下本项目的项目结构,下面我们会一一讲解。

 

3. 实体类

实体类的作用是存储数据并提供对这些数据的访问。在我们这个项目中,实体类统一被放到了model
包下,通常情况下,实体类中的属性与我们的数据表字段一一对应。当我们编写这些实体类的时候,建议对照着数据表的字段以防疏漏。

 

3.1 BaseModel

在我们数据表中,有几个公共的字段,可以提取出一个实体类的父类 BaseModel
,并提供 getter
setter
,源码如下:


package com.colorful.model;
import java.sql.Timestamp;
public class BaseModel {
    private Integer id;
    private Timestamp createTime;
    private Timestamp updateTime;
    private Timestamp deleteTime;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public Timestamp getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Timestamp createTime) {
        this.createTime = createTime;
    }
    public Timestamp getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Timestamp updateTime) {
        this.updateTime = updateTime;
    }
    public Timestamp getDeleteTime() {
        return deleteTime;
    }
    public void setDeleteTime(Timestamp deleteTime) {
        this.deleteTime = deleteTime;
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

值得注意的是,Timestamp
java.sql
下的类。

 

3.2 实体类编写

接下来,再在model
包下新建 3 个类:User
Goods
Category
,并提供getter
setter
。如下是每个类的代码:


package com.colorful.model;
public class User extends BaseModel {
    private String userName;
    private String nickName;
    private String password;
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getNickName() {
        return nickName;
    }
    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435
package com.colorful.model;
public class Goods extends BaseModel {
    private String name;
    private String description;
    private Integer categoryId;
    private Double price;
    private Integer stock;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Integer getCategoryId() {
        return categoryId;
    }
    public void setCategoryId(Integer categoryId) {
        this.categoryId = categoryId;
    }
    public Double getPrice() {
        return price;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
    public Integer getStock() {
        return stock;
    }
    public void setStock(Integer stock) {
        this.stock = stock;
    }
}
代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
package com.colorful.model;
public class Category extends BaseModel {
    private String name;
    private String description;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}
代码块12345678910111213141516171819202122232425

 

4. 实现用户鉴权

 

4.1 登录方式

想要使用系统进行商品管理,第一步要做的就是登录。

我们的系统使用用户名和密码进行登录校验,上一小节我们已经建立了imooc_user
表,并向表中插入了一个用户 admin
,其密码为 123456
。显然,通过如下SQL
就可以查询到该用户:


SELET * FROM `imooc_user` WHERE `username` = 'admin' AND password = '123456';
代码块1

如果查询到这个用户,就表示用户名密码通过校验,用户可执行后续操作,如果没有查到,就要提示用户重新输入账号和密码。

 

4.2 数据访问对象

我们先不管用户是如何输入账号密码的,接下来要编写的业务代码就是根据用户名和密码去查询用户。那么涉及到数据库查询的代码应该放到哪里呢?参考上面的系统架构图,DAO
是数据访问对象,我们可以在dao
包下面新建一个UserDAO
,并写入如下代码:


package com.colorful.dao;
import com.colorful.model.User;
import com.colorful.util.JDBCUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class UserDAO {
    public User selectByUserNameAndPassword(String username, String password) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        User user = new User();
        try {
            // 获得链接
            connection = JDBCUtil.getConnection();
            // 编写 SQL 语句
            String sql = "SELECT * FROM `imooc_user` where `username` = ? AND `password` = ? AND `delete_time` is null ";
            // 预编译 SQL
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数
            preparedStatement.setString(1, username);
            preparedStatement.setString(2, password);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                user.setId(resultSet.getInt("id"));
                String nickname = resultSet.getString("nickname");
                if (nickname.equals("")) {
                    nickname = "匿名";
                }
                user.setNickName(nickname);
                user.setUserName(resultSet.getString("username"));
            } else {
                user = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            JDBCUtil.release(resultSet, preparedStatement, connection);
        }
        return user;
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142434445464748

UserDAO
类下面有一个 selectByUserNameAndPassword()
方法, 接收两个参数 username
password
,返回值类型是实体类 User
,如果没有查询到,返回的是一个 null

完成了 UserDAO
的编写,我们需要到服务层 service
包下,新建一个 UserService
,并写入如下代码:


package com.colorful.service;
import com.colorful.dao.UserDAO;
import com.colorful.model.User;
public class UserService {
    private final UserDAO userDAO = new UserDAO();
    // 登陆
    public User login(String username, String password) {
        return userDAO.selectByUserNameAndPassword(username, password);
    }
    
}
代码块123456789101112131415

到这里大家可能有些疑问,这个类下面的login()
方法,直接调用了我们刚刚编写的 DAO
下面的 selectByUserNameAndPassword()
方法,为什么还要嵌套这么一层么?这不是多此一举么?

要讨论 service
层的封装是不是过度设计,就要充分理解设计服务层的概念和意义,服务层主要是对业务逻辑的封装,对于更为复杂的项目,用户登录会有更多的方式,因此在服务层,会封装更多的业务逻辑。如果没有服务层,这些复杂的逻辑不得不都写在数据访问层,显然这是不合理的。我们现在这个项目没有使用任何框架,等到后面大家学习了Spring
这种框架,一定会对这样的分层的好处有所体会。

 

4.3 使用 Scanner 类与用户交互

完成了上面一系列的封装,就剩下我们和用户的交互了,本项目中,我们使用 Scanner
类来接收用户的输入,并使用print()
方法向屏幕输出。

打开 App.java
入口文件,创建UserService
实例,编写一个主流程方法 run()
,并在入口方法 main()
中调用该方法:


package com.colorful;
import com.colorful.model.User;
import com.colorful.service.UserService;
import java.util.Scanner;
/**
 * @author colorful@TaleLin
 * Imooc Goods
 */
public class App {
    private static final UserService userService = new UserService();
    
    /**
     * 主流程方法
     */
    public static void run() {
        User user = null;
        System.out.println("欢迎使用商品管理系统,请输入用户名和密码:");
        do {
            Scanner scanner = new Scanner(System.in);
            // 登录
            System.out.println("用户名:");
            String username = scanner.nextLine();
            System.out.println("密码:");
            String password = scanner.nextLine();
            user = userService.login(username, password);
            if (user == null) {
                System.out.println("用户名密码校验失败,请重新输入!");
            }
        } while (user == null);
        System.out.println("欢迎您!" + user.getNickName());
        // TODO 登录成功,编写后续逻辑
    }
    public static void main( String[] args )
    {
        run();
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142

run()
方法中有一个 do ... while
循环,循环的条件是 user
对象为 null

我们知道,do... while
循环会首先执行 do
中的循环体,循环体中创建了一个 Scanner
类的实例,获取到用户的输入后,我们会调用用户服务层的login()
方法,该方法返回实体类对象User
,如果其为 null
表示用户名密码校验失败,需要用户重新输入, user == null
,满足循环的条件,会一直执行循环体中的代码。直到循环体中的 user
不为 null
(也就是用户登录校验成功后)才终止循环。

下面运行App.java
main()
方法,如下为登录失败的截图:

如果用户名密码检验错误,就要反复输入用户名密码重新登录。

如下为登录成功的截图:

在本小节,我们成功搭建了项目工程,通过实现一个用户鉴权模块,介绍了整体的系统架构。我们在编写实体类的同时,复习了面向对象的继承性;在数据访问层,也复习了 JDBC API
的使用;在编写程序入口文件的同时,也复习了 Scanner
类的使用和循环的使用。

关于系统鉴权,这里还有一个待优化的地方,大家下去之后可以思考一下,在下一小节的开头,我将带领大家一起来优化。下一小节也将主要讲解最后剩余的商品模块和分类模块的实现,也会复习到很多其他方面的基础知识。


文章转载自编码逻辑,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论