雅客

往事随风


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

HelloBird——资源回收o2o平台

发表于 2018-04-21 | 分类于 我的项目 |
字数统计: 1,597 | 阅读时长 ≈ 6

项目介绍

 ”HelloBird”是一个基于微信小程序搭建的资源回收o2o平台,目的是为了实现废旧资源的流转。在”HelloBird”中,用户可以轻松的获取生活垃圾的基本信息,并能根据自己的实际情况便捷考虑是否将废品出售,在用户选择好要出售的废品并提交订单之后,系统将自动派送回收员上门服务,收取废品,使用户足不出户就能将生活中的垃圾得到妥善处理,还能获得一定的收益,并且实现了资源的再利用。

功能需求如下:

项目架构

 ”HelloBird”项目客户端采用微信web平台,使用JavaEE做后台架构,数据采用的是关系型数据库MySQL。在后台架构上,采用SSM(Spring+SpringMVC+Mybatis)框架,Mybatis负责持久层操作,SpringMVC作为前端控制器,处理前台和后台的交互,Spring作为IOC容器,为Mybatis和SpringMVC提供支持(事物操作等)。

架构图如下:

页面逻辑图如下:

项目中遇到的问题

  • 使用json进行前后台数据交互的格式: 该项目使用json作为前后台数据传输的格式,SpringMVC的两个注解非常友好的帮我进行封装于实现,封闭式@ResponseBody和@RequestBody,@ResponseBody负责将数据包装成json字符串返回给前台,@RequestBody则将从前台接收到的数据包装成对应的JavaBean对象。在使用过程中,出现了中文字符乱码问题,后来进行了解决。
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
1. 使用如下配置可解决字符乱码问题(全局设置):
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>

2.使用produces = "text/html;charset=UTF-8"也能解决中文乱码问题
@RequestMapping(value = "/regist", method = RequestMethod.POST, produces = "text/html;charset=UTF-8")
@ResponseBody
public Map<Integer, Object> regist(@RequestBody User user){

Map<Integer, Object> map = new HashMap<>();

try {
//用户注册
map = userService.regist(user);

}catch (Exception e){
System.out.println(e.getMessage());
}

return map;
}

3.在请求头中进行设置
response.setHeader("Cache-Control", "no-cache");
response.setContentType("text/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.write(result);

上面所讲的第二种方法中,关于produces(还有一个 consumes),可以查看此博客,做更深了解:

  • https://blog.csdn.net/walkerjong/article/details/7994326
  • Transfer-Encoding:chunked和Content-Disposition:inline;filename=f.txt问题: 在做项目的时候,响应头里面出现了 Transfer-Encoding:chunked和Content-Disposition:inline;filename=f.txt,让我百思不得其解,后来经过多方查阅,终于让我弄明白了到底是设么意思。

  1. Transfer-Encoding:chunked 简单来说,Transfer-Encoding是一个Http的头部字段,意思是传输编码。而chunked是分块传输的意思,就是说对于输出内容长度不确定的,如gzip格式等,就会用到分块传输技术。参考博客:

    • https://blog.csdn.net/whatday/article/details/7571451
    • https://imququ.com/post/transfer-encoding-header-in-http.html
  2. Content-Disposition:inline;filename=f.txt 初遇这个响应头的时候我也是有点懵,不知道什么意思,后来也搞明白了。该属性设置的是在文件下载时对下载文件的一个标识字段。参考链接:

    • https://blog.csdn.net/dabinge1/article/details/52587662
  • session问题和token的使用: 该项目是基于微信小程序的,在保存用户登录状态和身份验证这一点上,我首先想到了用session,同时我想项目比较小,可以把用户的购物车放在session中,一次会话结束就将session里面的购物车清除,后来我发现了这其中的问题。首先:微信小程序不支持Cookie(意味着session也用不了),这在项目后期对于我的打击肯定是很大的,因为我的好多业务逻辑都是基于session的,session用不了那么项目就瘫痪了一大半,后来经过冷静思考也发现的我设计的问题,关于session的问题只是其次,用户的购物车放在session中,一次会话后就被清除,这显然是不合理的,对于用户的信息,我们需要把它固话在数据库中,后来我又重新增添一张表来保存用户的购物车,这个逻辑想明白之后,还有一点不知道怎么弄,session用不了,那用什么好呢?后来我想到了使用token。
    • token是什么:token是一个加密的字符串,里面包含了后台给前台传递的加密信息(不容易被破解)
      • https://ninghao.net/blog/2834
      • https://dombro96.github.io/2017/12/11/%E6%B5%85%E8%B0%88token/

一点总结——我的心里话

 说实话,自己真正做完一个项目确实挺开心,特别是在把每一个接口都测试好,并且项目真正跑起来的时候,感觉身体特别放松,觉得付出的一切都值得。作为项目负责人,需要考虑的事情特别多,一边需要和团队成员沟通,讨论需求,讨论接口,另一边还需要思考如何进行后台的整个架构,还要关心项目的进度等等,虽然团队在磨合过程中确实出现了很多问题,比如由于前期的交流不充分,需求讨论的不清楚,导致后面的接口改了好多次。还有比如原先对微信小程序的机制不是太了解,差点导致项目崩盘等等。但好在我们都坚持了下来,不管这个项目做得好不好,但我们起码还是把它完成了,总的来说还是要谢谢我们的团队,谢谢这次做项目的经历,让我得到了锻炼与成长,同时也让我明白了作为一个项目负责人,应该明白如何和组员进行良好的沟通,应该有一个清晰的项目进度计划,应该制定一个完善的奖惩措施,应该统筹安排好每个分支的工作。确实,一次历练就是一次成长,一次成长对于整个人生来说弥足珍贵!

Java 8 新特性——lambda表达式

发表于 2018-04-21 | 分类于 Java基础 |
字数统计: 2,052 | 阅读时长 ≈ 8

什么是lambda表达式?

1.lambda 表达式是一个可传递的代码块,可以在以后执行一次或者多次 —–>《Java核心技术》
2.lambda 表达式是一个匿名函数

为什么使用lambda表达式?

先看一个例子,这是一个典型的比较器,使用这个比较器可以对任意字符串按长度进行排序。

1
2
3
4
5
6
7
8
9
10
11
Comparator<String> com = new Comparator<String>() {  
@Override
public int compare(String first, String second) {

return first.length()-second.length();
}
};

String[] str = {"saddsa", "sdadcsa","rgeavrr", "csfdcwe"};//如果使用Arrays类中的sort()方法,只能按照字典顺序排序。假如想要对其
//按照长度排序,需要传入比较器
Arrays.sort(str, com);

分析这段代码,其实我们不难发现,在执行排序方法的时候。compare() 方法并非只执行一次,在数组完成排序之前,sort() 方法会一直调用compare() 方法,只要元素的顺序不正确就会重新排列元素,将比较所需的 代码块(s1.length()-s2.length()) 放在 sort() 方法中,与其他的排序逻辑集成然后完成排序。也就是是说,这里存在一个不断回调的过程。

这么一分析,我们发现,真正参与排序逻辑的只是 代码块(s1.length()-s2.length()) 罢了。也许我们会想,为什么不直接给 sort() 方法传递这个代码块,而需要给它传递一个 Comparator 实例呢?这样不会显得过于繁琐吗?实际上,在Java 8 之前,我们也只能这样去做,Java是面向对象的语言,假若我们需要传递某段代码块,必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。难道就没有解决的办法了?当然不是!

Java 8 推出了 lambda 表达式,能非常完美的解决这个问题,可以使代码变得更加简洁,灵活,优雅。(这时你可以思考一下Java语言为什么美?)

废话不多说,直接上代码

1
2
3
4
5
6
7
8
9
  //第一种方式
Comparator<String> com = (first, second) -> first.length() - second.length();

String[] str = {"saddsa", "sdadcsa","rgeavrr", "csfdcwe"};

Arrays.sort(str, com);

//第二种方式
Arrays.sort(str, (first, second) -> first.length() - second.length());

看到没有,看到没有,这就是差距!你那么多废话,我一句就搞定,而且我效率还比你高,这就是强!
现在,让我们一起进入 lambda表达式 的世界,请在开始之前想一想刚才我所说的:lambda表达式传递的是代码块(函数),请暂时忘掉对象

如何使用lambda表达式

  1. -> :lambda 操作符,它将 lambda表达式 分为两个部分
     左侧:指定 lambda表达式 所需要的所有参数
     右侧:lambda表达式的主体,即 lambda表达式 要执行的功能
  1. lambda表达式语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//方式一
Runnable r1 = () -> System.out.println("Hello World");//即使没有参数,也要提供括号

//方式二
Consumer<String> fun = (args) -> System.out.println(args);//方法带参数,需要传入一个参数
Consumer<String> fun = args -> System.out.println(args);//lambda 只需要一个参数时,参数的小括号可以省略

//方式三
BinaryOperator<long> bo = (x, y) -> { //lambda 需要两个参数,并且有返回值
System.out.println("实现接口");
return x + y;
};
BinaryOperator<long> bo = (long x, long y) -> x + y;//当lambda体只有一条语句时,return和大括号可以省略

BinaryOperator<long> bo = ( x, y) -> x + y;//类型可以省略,Java可以通过上下文推断得出,称为类型推断

函数式接口——理解 lambda表达式的关键

任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
 注意:这里说函数式接口必须有一个抽象方法也许会使大家疑惑。我们会想接口中所有的方法不都是抽象的吗?
实际上,接口完全有可能重新 声明Object类的 toString, clone 等方法,这些声明有可能会让方法不再是抽象的。
不过,默认方法和静态方法不会破坏函数式接口的定义

1
2
3
4
5
6
@FunctionalInterface//该注解会强制检查该接口是不是一个函数式接口
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() { //允许默认方法
}
}

lambda表达式只支持函数式接口,因为lambda表达式到最后只能转换为函数式接口。其实就我个人理解而言,这是一种必要的转换,lambda表达式本来就是为了方便某个重复性的功能,使对象的概念弱化,凸显逻辑代码的地位(也就是代码块)。如果不是函数式接口,你弄他好几个方法,lambda表达式总不能一次性全部去执行吧,这在逻辑上也是行不通的。这纯属是我对函数式接口的个人理解,也许会有误,欢迎讨论。

方法引用与构造器引用

前面说过,如果 lambda表达式 的代码块只有一条代码,可以省略花括号。同样的,你也可以使用方法引用和构造器引用。使用方法引用和构造器引用可以使代码阅读性更强,更简洁。

  • 方法引用 戳一戳 1 戳一戳 2
    方法引用有四种情况(在Java核心技术中分为三种情况,这里我把构造器引用也归入方法引用之中,因为实际上构造器不也是方法嘛)
    1. object::InstanceMethodName 引用对象的实例方法
    2. ClassName::staticMethodName 引用类的静态方法(普通类)
    3. Class::instanceMethodName 引用类型对象的实例方法(比如String)
    4. ClassName::new 引用构造方法
1
2
3
4
5
6
7
8
9
10
11
Timer t = new Timer(1000, ele -> System.out.println(ele));//添加一个定时器,每隔1秒打印一次 ele

//使用方法引用可以写成(与上面的等价)
Timer t = new Timer(1000, System.out::println);//直接将 println 方法传递给构造器


对字符串排序
Arrays.sort(str, String::compareToIgnoreCase);//等价于 (x, y) -> x.compareToIgnoreCase(y)
//类似的 Math::pow 等价于 (x, y) -> Math.pow(x, y);

假如有多个重载的同名方法,编译器会尝试从上下文找出你指的那一个方法。不知不觉,Java又强势装逼一波。
  • 构造器引用——主要与Stream结合使用:与方法引用类似,将方法名换为new。
1
2
3
4
//有一个字符串列表,将其转换为Person对象数组,需要在字符串上调用构造器
ArrayList<String> names = .....;
Stream<Person> stream = names.stream().map(Person::new);//Person::new 是 Person构造器 的一个引用
List<Person> people = stream.collect(Collection.toList);
  • 数组也可以建立构造器引用
1
2
3
4
int[]::new 等价于 x -> new int[]

//将流中的对象转换为对象数组
Person[] person = stream.toArray(Person[]::new);

处理lambda表达式

通过上面的介绍,我们或许对lambda表达式有了进一步的理解,现在我们将学会怎样去使用lambda表达式。

使用lambda表达式的重点是为了延迟执行,下面是使用lambda表达式的几个场景
1.在一个单独的线程中运行代码
2.多次运行代码
3.在算法的适当位置运行代码(排序中的比较操作)
4.发生某种情况时执行代码(点击按钮数据到达)

其实,我们可以想一想,这些情况大多在哪里出现呢?想到排序,想到比较,这很容易让我们联想到集合和数组。嗯,就我现阶段的水平而言,或许我只能理解lambda表达式在集合和数组中给我带来的便利,但我想这也就足够了。

1
2
3
4
5
6
7
//这就是一个比较常规的用法,可以把lambda表达式作为参数传递给方法,而且需要注意,lambda表达式的参数必须是一个函数式接口
public static void repeat(int n , Runnable action){
for(int i = 0 ; i < n; i++)
action.run();
}

repeat(10, () -> System.out.println("Hello, world"));

迭代器模式——Iterator

发表于 2018-04-16 | 分类于 设计模式 |
字数统计: 2,722 | 阅读时长 ≈ 11

关于迭代器(Iterator)

 迭代器这个名词想来我们已经不陌生了,在集合中我们就学习过迭代器,并掌握了如何使用迭代器来遍历集合中的元素。

1
2
3
4
5
6
7
8
9
10
11
12
例如:
List<String> list = new ArrayList<>();
list.add("anc");
list.add("cvl");
list.add("uio");
list.add("ope");

Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.print(list.next());
}
输出:anc cvl uio ope

 实际上,迭代器就是反复同一件事的一个循环,就如同for(int i = 0; i < 100; i++)中的i++,只要符合条件就加1,直到遍历完每一个元素后就退出,而迭代器模式(Iterator模式)的思想也就是来自于此。迭代器模式是一种没落的模式,除非产品性质的需要,否则不会单独去写一个迭代器模式(Java中的集合就对迭代器模式进行优秀的封装,基本上不用认为扩展,都能满足我们的需求)。我们可以看看迭代器的定义:迭代器提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的细节。通俗点说,迭代器的诞生就是为遍历容器(容器就是能持有对象的集合,如Collection,Set)中的元素服务的。

迭代器模式——你在不在?我要揍你一顿

 举一个例子,详细讲讲什么是迭代器模式

 在一个风和日丽的早晨,阳光明媚,春暖花开,打开古老的藏书阁,我要找一本心仪的古书陶冶一下情操,可是让我很郁闷的是,由于时间太久没人整理,很多书都凌乱的放在书架上,所以我费了老大的劲儿才把书籍找到。我就在想,要是有一个这样的物件就好了,它能在我想要从一堆杂乱无章的事物中寻找某一件东西时,它能按整齐的把它排列(排列顺序自己选择)好,这样我找的时候不就容易多了,我冥思苦想很久,找了很多上古典籍查阅(《设计模式之禅》,《图解设计模式》)中找到了答案,那就是上古秘书——迭代器模式。

 现有一个书架BookShelf,有一堆书Book,我想把书放到书架上,并按照书的名字按顺序显示出来。看一下示例图

程序的类图如下

类和接口

逐步剖析(请直接看代码)

Aggregate接口:表示集合的接口,相当于Java集合中Collection,List等集合接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 表示集合的接口
*/
public interface Aggregate {

/**
* 该方法用于生成一个遍历结合的迭代器
*
* 在遍历集合中的元素时吗,调用iterator方法生成一个实现了Iterator接口的类的实例
* @return
*/
public abstract Iterator iterator();

}

Iterator接口:表示用于遍历集合的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 用于遍历集合的接口:遍历集合中的元素,其作用相当于循环语句中的循环变量
*/
public interface Iterator {

/**
* 判断是否存在下一个元素
* @return
*/
public abstract boolean hasNext();

/**
* 返回集合中的一个元素
* @return
*/
public abstract Object next();

}

BookShelf类:实现了Aggregate接口,表示一个书籍的集合,可以看成Java集合框架中的实现类

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
/**
* 表示书架类,可以看成是一个书的集合,所以需要实现Aggregate接口
*/
public class BookShelf<T> implements Aggregate {

private Book[] books;// 可见性设置为private,避免不小心篡改
private int last;//标识最后一本书的索引

public BookShelf(int maxsize) {
this.books = new Book[maxsize];// 生成BookShelf实例的时候指定了books的大小
}

//根据索引得到书籍
public Book getBookAt(int index) {
return books[index];
}

//向书架上添加书籍
public void appendBook(Book book) {
this.books[last] = book;
last++;
}

//返回书籍的数量
public int getLength() {
return last;
}

@Override
public Iterator iterator() {
return new BookShelfIterator<T>(this);
}

}

BookShelfIterator类:Iterator的实现类

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
/**
* 遍历书籍的迭代器实现(Iterator)类
* @param <T>
*/
public class BookShelfIterator<T> implements Iterator {
private BookShelf<T> bookShelf;//表示BookShelfIterator要遍历的书架
private int index;//表示迭代器当前指向的书的索引

//初始化
public BookShelfIterator(BookShelf<T> bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}

@Override
public boolean hasNext() {
<!-- if (index < bookShelf.getLength()) {//如果当前索引小于书架书籍的数量,表名下一本书还存在,继续遍历
return true;
} else {
return false;
} -->

return index != bookShelf.getLength;
}

/**
* next()方法有两个作用:
* >返回当前索引对应的书籍实例
* >并将index指向下一个元素 >>>> 这和for循环中的i++很像
*/
@Override
public Object next() {
Book book = bookShelf.getBookAt(index);//将对应索引的书一一返回
index++;
return book;
}
}

Book类:书籍类(实体类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Book {

private String name;

public Book(String name) {
super();
this.name = name;
}

public String getName() {
return name;
}

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

@Override
public String toString() {
return "Book [name=" + name + "]";
}

}

下面是Main方法

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
public class Main {

public static void main(String[] args) {

BookShelf<Book> bookShelf = new BookShelf<>(4);

bookShelf.appendBook(new Book("雪国"));
bookShelf.appendBook(new Book("山中古音"));
bookShelf.appendBook(new Book("天堂"));
bookShelf.appendBook(new Book("一个人的路"));

Iterator iterator = bookShelf.iterator();

while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}

}

运行结果:
Book [name=雪国]
Book [name=山中古音]
Book [name=天堂]
Book [name=一个人的路]

集合框架中的迭代器模式(源码解析)

 上面是我们自己写的迭代器模式,用我们自己的想法实现了一个简单的迭代器,怎么样,你对迭代器了解有多少了呢,如果你还是不清楚,那我们再来看看Java集合框架是如何运用迭代器的。我将主要以源码的方式解析,你会发现其实和我上面写的差不多。

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
我就使用List接口进行解析,List定义了如下方法
Iterator<E> iterator();

Iterator接口:
public interface Iterator<E> {

boolean hasNext();

E next();

default void remove() {
throw new UnsupportedOperationException("remove");
}

//该方法与函数式接口有关(lambda表达式)
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

以List的一个实现类ArrayList来看看如何实现的

public Iterator<E> iterator() {
return new Itr();
}

//Itr是AbstractList的一个成员内部类(其实ArrayList也有一个Itr的成员内部类
只不过这是优化版本)
private class Itr implements Iterator<E> {
int cursor; //表示下一个要访问的元素的索引
int lastRet = -1; //表示上一个元素的索引
int expectedModCount = modCount;//表示对ArrayList修改次数的期望值,它的初始值为modCount(modCount是AbstractList类中的一个成员变量)

public boolean hasNext() {
return cursor != size();
}

public E next() {
checkForComodification();
try {
int i = cursor;//下一个元素的索引
E next = get(i);//通过该索引得到实例
lastRet = i;//将该索引赋值给表示上一个元素索引的变量
cursor = i + 1;//指向下一个元素
return next;//返回当前实例
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

深入研究一下

 OK,上面就是迭代器在集合框架中的使用,现在我们或许就理解了为什么在学习集合的迭代器器时说,我们是去获取一个迭代器,而并非是我们new一个迭代器对象,下面我们来看看迭代器模式的类图,并分析一下我们为什么要使用迭代器

  • Iterator(迭代器):该角色定义了顺序逐个遍历元素的接口
  • ConcreteIterator(具体的迭代器):实现Iterator接口
  • Aggregate(集合):定义创建Iterator角色的接口
  • ConcreteAggregate(具体的集合):该角色负责实现Aggregate所定义的接口

 看了上面介绍的迭代器模式,我们或许头有点晕,既然同是迭代,为什么我们不去使用更为普通的迭代方式,如for循环呢,非要用这种复杂的设计模式干什么,其实回到前面迭代器模式的定义:迭代器模式提供一种顺序访问一个聚合对象中各个元素的方法,而又不暴露该对象的内部实现。

举个例子:我是车站的售票员,对于我来说,我只是负责售票的,其他的事情我一概不管,对于我来说,只有一个原则,那就是只有买了票才能乘车,不管你是中国人外国人,不管你是小偷还是杀人犯,只要你没买票就不能乘车,我不用管乘车对象是什么,我只管你有没有买票。

 如同上面举的那个例子,这里只是用了Iterator的hasNext和next方法,并没有调用BookShelf的方法,也就是说,while循环并不依赖于BookShelf的实现。不管BookShelf如何变化,只要BookShelf的iterator方法能返回正确的Iterator实例,即使不对while循环做任何修改,都可以正常工作。设计模式的作用就是帮助我们编写可复用的类。而可复用的含义就是将类视为“组件”,当一个组件方法发生变化时,不需要对其他组件进行修改或者只需要进行很小的修改就可以了,大大提高了代码的重用率。

1
2
3
while(iterator.hasNext()){
System.out.println(iterator.next());
}

结语

迭代器模式是一种古老的设计模式,也是一种非常常见的设计模式,它已经完美的镶嵌于聚合关系中,如Java的集合框架,几乎每一个集合类都对迭代器模式进行了封装。正是由于它的普遍性,所以很多大佬都提出将迭代器模式从23中设计模式中删除。不管迭代器模式在未来是否还能存在于设计模式家族中,我们只需要知道,应用之广泛则证明其价值,所以值得我们去学习研究它的思想精髓。

推荐书籍与网站:

  • 《图解设计模式》 《大话设计模式》 《设计模式》 《Java设计模式》
  • http://www.cnblogs.com/dolphin0520/p/3933551.html

说说UML

发表于 2018-04-15 | 分类于 设计模式 |
字数统计: 73 | 阅读时长 ≈ 1

UML——继承

UML——实现

UML——聚合

聚合表示持有对象,是一种一对多的关系,比如图中的Basket中定义了一个Fruits的数组,表示可以持有Fruit的多个对象

推荐链接

  • https://blog.csdn.net/qq_31655965/article/details/54645220
  • https://www.cnblogs.com/scevecn/p/5663369.html

Java中的引用

发表于 2018-04-13 | 分类于 Java基础 |
字数统计: 3,559 | 阅读时长 ≈ 13

java中的引用

 在Java中,引用是一种神奇的东西,通过引用我们可以完成很多事情。习以为常的我们往往忽略了一些本质的东西。我们浅显的以为引用无非就是用于对象调用的,真的是这样吗?你真正了解了Java中的引用了吗?让我们透过现象看本质,去了解一些更深的东西。在讨论引用之前,我们先聊聊对象这种东西。

对象——一个神奇的物种(我无所不能)

 看了这张图也许大家都对对象这玩意儿有了很深刻的印象了,以前一直问对象是什么,对象长什么样,好吧,我告诉大家,对象就长这样(哈哈哈哈。。。。此处省略一万字)。为什么说对象是一个很神奇的物种,就我自己的理解,给出如下解释:

  • 封装的完美性:对象是一个实实在在的个体(在堆中有自己的空间),就如同我们一个完整的人一样,都在这大千世界占据着一个空间。一个对象的生成和一个人诞生很相似,都要经过一个异常复杂的过程。人需要十月怀胎,而一个对象从Java虚拟机接到命令(new : 喂,生一个对象出来)到创建一个对象出来需要经历很大周折,有的需要“千万年”,有的需要“几亿年”,有的可能直接挂掉(想想人家CPU的时间周期,我们的1秒就是人家的百亿年了。。。)。怎么样,对象的创建是不是很不容易,所以创建对象一定要慎重(反射技术就能提前对对象进行剖腹产,很多框架都使用了这种技术,其中Spring中的依赖注入(DI)就是对这种技术的深刻运用。。。)。好了,扯皮了半天,知道了对象是咋来的,该说说为啥说封住的完美了,其实还是类比我们人一样,对象头就好比人的头一样,人要靠五官观四方,听八方,对象也需要他存储的一些比如哈希码,指向类元素的指针等来定位一个对象等等,实例数据区就如同我们的身体四肢,大家可以脑补我们可以干什么。就是由于这种近乎偏执严谨完美的封装,才能让对象在Java世界里徜徉,无所不能。
  • 关于对象无所不能的特性,我就不多赘述了,随着学习的深入,大家会慢慢发现,没有对象干不了的事,或许我该这样表达,我们下意识的都会想着用对象去干些什么,也许这就是Java设计的初衷——万物皆对象。

好了,扯了这么半天,也该说说引用是啥玩意了,在具体的说引用之前,还是放两张图,暖暖肠胃。
下面是访问对象的两种方式,也是指针最直观的运用。

  • 使用句柄访问

使用句柄访问,Java堆中会划分出一块内存来作为句柄池,而reference中存储的就是句柄的地址,句柄包含了对象实例数据与类型数据各自的具体地址信息。

  • 优势:reference中存储的是稳定的句柄地址,在对象被移动时(比如Java虚拟机在执行垃圾回收时就经常移动对象)时只会改变实例数据指针,而reference本身不需要修改。
  • 劣势:时间太慢,相比于直接指针访问,多了一次定位开销。
  • 直接指针访问

使用直接指针访问:reference直接指向Java堆中的实例数据,reference中存储的直接是对象的地址。和第一种对比,优劣可自行观之。

 这里也简单说一下为什么会有这两种定位方式:Java程序需要通过栈上的reference来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。

引用是啥玩意儿

 在Java学习的基础阶段,我们对引用的概念只停留在初级阶段:也就是引用代表一个对象,或者引用中存放着对象在堆内存中的地址,通过引用可以找到这个对象,并操作对象上的实例数据。这种理解本没有错,不过只是停留在比较浅显的层次。其实,从更深的角度去理解,引用在Java中有着举足轻重的地位。

 以前我们在学习C, C++的时候,对于开辟一段内存是一件颇为麻烦的事情,我们动态申请了一段内存,就需要在适当的位置把他free或者delete掉,否则就会出现很多溢出等问题,这就需要程序员更多的去关注内存问题,小心的检查每一份开辟的内存是否关闭,在哪个地方关闭,想想就很痛苦。Java是面向对象的语言,对象的产生就是内存的开辟,这就意味着内存的申请是频繁发生的,在Java中,一个new关键字就代表了一块内存的申请。我们平时new一个对象出来很舒服,而且也不用关心这个对象的内存在什么时候释放,只要尽管用就行了,那我们是否想过,why?

 其实这与Java虚拟机的垃圾回收机制有关,垃圾回收机制会帮我们自动的清理一些无用的对象,保存有用的对象,关于垃圾回收机制是很么,垃圾回收算法有哪些,在这里将不多赘述,有兴趣的可以等待我的后续更新。言归正传,说了这么半天,其实大家也应该猜到了,引用与对象的回收有着非常密切的关系,可以说,正是有了引用这玩意儿,垃圾回收才能如此给力(在这里小小的提一下:判断对象是否存活的两种算法——引用计数算法和可达性分析算法)。OK了,说了这么半天引用的厉害之处,也该好好说说什么是引用了。

 在JDK1.2之前,Java中引用的传统定义如下:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(估计很多人的理解也就到这了……)在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为了四种,分别是:强引用(Strong Reference), 软引用(SoftReference), 弱引用(WeakReference), 虚引用(PhantomReference), 这四种引用强度依次逐渐减弱。下面分别举例介绍这四种引用:

  • 强引用(StrongReference): 强引用是最常见的一种引用,在Java中百分之九十九的都是常引用,通常用new关键字new出来的对象都指向一个强引用。例如Object object = new Object(),object就是new Object()这个对象的强引用,我们知道在Java中,对象是可以被多个引用指向的,只要有一个强引用还在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用(SoftReference): 软引用是用来描述一些还有用但非必须的对象,强度上弱于强引用,在java.lang.ref包下的SoftReference类维护者软引用。它的作用是告诉垃圾回收器,程序中哪些对象是不那么重要的,在内存空间充足时可以被保留,而在内存空间不足时将会被暂时回收。软引用非常适合创建缓存,在系统内存不足时,缓存将会被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如下就是Java软引用的一个简单示例

//person强引用
Person person = new Person();

System.out.println(person);

//将person这个强引用包装成弱引用
Reference<Person> reference = new SoftReference<Person>(person);

//强引用为空,只剩下弱引用
person = null;

System.gc();

//使用get()方法来获取软引用所指向的对象
System.out.println(reference.get());

//运行结果:
com.reference.Person@7852e922
com.reference.Person@7852e922
  • 弱引用(WeakReference): 弱引用也是用来描述非必须对象的,强度上弱于软引用,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在java.lang.ref包下的WeakReference类维护着弱引用。弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系,最常见的用途在于集合类中,特别是哈希表中(典型的是HashMap)。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。对于这种情况就使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。
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
Integer i = new Integer(1);

HashMap<Integer, Person> w = new HashMap<>();

//将i这个强引用包装成弱引用
WeakReference<Integer> in = new WeakReference<>(i);

i = null;

//将包装后的引用放入HashMap中
i = in.get();
w.put(i, new Person("xx", 12));

System.out.println("Before gc:" + in.get());

//开启垃圾回收
System.gc();

System.out.println("After gc:" + in.get());

结果:Before gc:1
After gc:null


//使用Java中WeakHashMap就可以避免上面所说的情况,它会自动将键值包装为弱引用类型

WeakHashMap<Integer, Person> weak = new WeakHashMap<>();
  • 虚引用(PhantomReference): 虚引用也成幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生命周期构成影响,也无法通过一个虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在java.lang.ref包下的PhantomReference维护着虚引用。

    其实虚引用是一个很偏的知识,与Java的对象终止化机制有关。大家可以去了解,在Java中有一个finalize方法,设计初衷就是一个对象在真正被回收前,执行一些清理的工作(就如同C++中的析构函数)。但是垃圾回收的运行时间是不用固定的,所以清理工作也不是提前预知的。而虚引用就可以解决这个问题,在创建一个虚引用的时候必须指定一个引用队列。当一个对象的finalize方法被调用了之后,这个对象的虚引用就会被加入到队列中,通过检查该队列中的内容就可以知道一个对象是不是准备要被回收了。

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
//缓冲区代码实现展示
public class PhantomBuffer {
private byte[] data = new byte[0];
private ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
private PhantomReference<byte[]> ref = new PhantomReference<byte[]>(data, queue);
public byte[] get(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Wrong buffer size");
}
if (data.length < size) {
data = null;
System.gc(); //强制运行垃圾回收器
try {
queue.remove(); //该方法会阻塞直到队列非空
ref.clear(); //幽灵引用不会自动清空,要手动运行
ref = null;
data = new byte[size];
ref = new PhantomReference<byte[]>(data, queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return data;
}
}

解释:每次申请新的缓冲区时,都要确保之前缓冲区的字节数组已经被成功回收。引用队列中的remove()方法会阻塞直到新的虚引用被加入到队列中。
  • 引用队列(ReferenceQueue): 在有些情况下,程序会需要在一个对象的可达到性发生变化的时候得到通知。比如某个对象的强引用都已经不存在了,只剩下软引用或是弱引用。但是还需要对引用本身做一些其他的处理。典型的情景是在哈希表中。引用对象是作为WeakHashMap中的键对象的,当其引用的实际对象被垃圾回收之后,就需要把该键值对从哈希表中删除。有了引用队列(ReferenceQueue),就可以方便的获取到这些弱引用对象,将它们从表中删除。在软引用和弱引用对象被添加到队列之前,其对实际对象的引用会被自动清空。通过引用队列的poll/remove方法就可以分别以非阻塞和阻塞的方式获取队列中的引用对象。

OK,讲到这,Java中的引用也就结束了,这篇文章主要就讲了Java中的引用,在其他的语言中也有引用的概念,比如C++中。但我没有对它们进行比较,因为个人觉得没有比较的意义,Java中引用和C++中的引用是两种不同的概念。如果大家实在想了解两者的区别,可以点击下面的第一个链接,看看人家写的,我就懒得写了。。。。

推荐链接

  • https://blog.csdn.net/u014082714/article/details/47701519
  • http://www.infoq.com/cn/articles/cf-java-garbage-references
  • https://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
  • https://juejin.im/post/5a72762c6fb9a01cbe65a4eb

Java 8 的新特性——使用Stream API来处理集合

发表于 2018-04-13 | 分类于 Java基础 |
字数统计: 4,329 | 阅读时长 ≈ 17

1. 什么是Stream(“流”)?

官方正解“流”的概念:流是数据的渠道。流代表了一个对象序列。流操作数据源,如数组或集合。流本身不存储数据,而只是移动数据,在移动过程中可能会对数据执行过滤,排序或其他操作。然而一般来说,流本身不修改数据源(集合,数组,I/O channel, 产生器generator )对流排序不会修改数据源的排序;相反,对流排序会创建一个新流,其中包含排序后的结果。
通俗理解(浅显): Stream 是元素的集合,类似于集合中的迭代器——Iterator,不过是高级版本的迭代器


理解流是什么
流是什么

从图中我们就可以鲜明的看到流是怎么工作的:

  • goods 集合提供了元素序列的数据源,通过 stream() 方法获得 Stream
  • filter / sorted / limit 进行数据处理,“连接起来” 构成 “流水线”
  • forEach 最终执行
1
2
3
4
5
6
//代码示例
goods.stream()
.filter(c -> c.getPrice() > 500 && c.getSales() < 200)
.sorted(Comparator.comparing(Good::getPrice).reversed())
.limit(10)
.forEach(c -> { c.setPrice(c.getPrice() / 2); });

通过对比集合中的迭代器我们会发现,流的理念更加清晰与透彻,容易理解,工作效率更高。

个人想法:集合好比是一个储物空间(容器),什么东西(对象)都可以往里面放,当我们需要的时候就往里面取,怎么取?用标签(索引)去取,OK。那假如我们需要找出里面最特变的那个,或者从中筛选出符合某种特征一类,或者给他们分分类,排排序什么的我们该怎么办?是不是需要一遍遍的去翻找,这就很麻烦。假如我们换一种思路,我们有一个流水线(管道),在需要对集合中的元素进行操作的时候,我们就把这些元素放到这个流水线上(变成流),我们需要进行诸如过滤,排序等操作时,只需要在这条流水线上同时操作,那么工作效率将会得到很大的提高。

不留误区:很多时候我们会有这样的误区,既然集合是用来存储对象的,那么流也是用来存储对象的,其实个人认为这种认知是不地道的。流其实是为了方便操作集合中的元素而存在的,流是一种一次性消耗品(就比如岁月如水,过了就是过了),进行了一次操作,完成了它的使命,自然也就消亡了,所以自始至终数据都还好端端的保存在数据源中(数组或集合中),这就是为什么说流本身不修改数据源

加深印象

  • 不存储数据。 流不是一个存储元素的数据结构。 它只是传递源(source)的数据。
  • 功能性的(Functional in nature)。 在流上操作只是产生一个结果,不会修改源。 例如filter只是生成一个筛选后的stream,不会删除源里的元素。
  • 延迟搜索。 许多流操作, 如filter, map等,都是延迟执行。 中间操作总是lazy的。
  • Stream可能是无界的。 而集合总是有界的(元素数量是有限大小)。 短路操作如limit(n) , findFirst()可以在有限的时间内完成在无界的stream
  • 可消费的(Consumable)。 流的元素在流的声明周期内只能访问一次。 再次访问只能再重新从源中生成一个Stream

2. Java 8 为什么引入 Stream API

  • 在使用集合中的Iterator遍历集合,完成相关聚合应用逻辑操作时效率低下,笨拙
  • 与lambda表达式结合可以对集合对象进行各种非常便利,高效的聚合操作,或者大批量的数据操作
  • 提供串行和并发两种聚合操作模式,并发能充分利用多核处理器优势,加速处理过程,利于写出高性能的并发程序。

Stream是一个函数式语言+多核时代综合影响的产物

3. 什么是聚合操作

或许第一次听说“聚合操作”这个术语时很多人都会觉得陌生,误以为这是一种多么高大上的操作。实际上,“聚合”一直广泛的应用于程序员的开发之中,比如关系型数据库中的一些操作就是“聚合操作”:

  • 客户每月平均消费金额 —— 平均值
  • 最昂贵的在售商品 —— 最值
  • 本周完成的有效订单 —— 去除特殊值
  • 取十个数据样本作为首页推荐 —— 截取一段数据

典型事例:在关系型数据库中,我们可以使用sql语句的 sum max min avg distinct 等函数实现聚合操作

加深理解:聚合操作(也称为折叠)是接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的聚合操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。

4. 流的两种操作类型

4.1 了解原理

  • Intermediate(中间操作):中间操作可以用来执行一系列动作的管道。一个流后面跟随零个或多个中间操作(Intermediate),主要目的是为了打开流,做出某种程度的数据映射(过滤),然后返回一个新的流,交给下一个操作使用。这类操作是惰性化的(lazy),也就是说,仅仅调用这类方法,并没有真正开始流的遍历。换句话说,中间操作不是立即发生的,相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生——这种机制称为延迟行为,所以中间操作是延迟发生的,延迟行为让流 API 能更加高效地执行。
  • Terminal(终端操作):终端操作会消费流,该操作用于产生结果,例如找出流中的最值。一个流中只能有一个terminal操作,当这个操作执行完成后,流就被消费光了,表示流已经死亡,无法使用。所以终端操作必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果。
  • short-circuiting(短路操作):尽早结束对流的操作,不必检查所有元素
    • 对于一个Intermediate操作,接受一个无限大的流(infinite/unbounded)的Stream,返回一个有限的Stream。
    • 对于一个Terminal操作,接受一个无限大的Stream,但能在有限的时间中计算出结果。

深度剖析,不留遗憾:
在中间操作原理讲解中,有一句话标注了黑体。如果我们细心一点的话也许会发现一个问题:终端操作是一个遍历流的过程,意味着流的死亡,那为什么还说中间操作是在其创建的流在执行终端操作后才执行?不是说流被消耗后就不能使用了吗?为什么这种延迟执行反而提高了效率呢?难道这句话有矛盾?其实,这句话是绝对正确的,且听我娓娓道来:

  • 延迟执行的效率:其实,在原理中解释的已经很清楚了,中间操作是 lazy 的,多个中间操作(诸如排序,过滤等)只会在Terminal操作的时候融合起来,一次循环完成。简单理解为,Stream里有个操作函数的集合,每次中间操作就把转换函数放入到这个集合中,在Terminal操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。(想想生活中的例子,建筑工地施工,是等材料到齐了开工效率高还是来了一部分材料就开工效率高)
  • 终端操作执行完后执行中间操作:或许理解了延迟执行也就理解了这一点,就不过多赘述了。你只要清楚一点,效率至上,谁先执行,谁后执行,效率说了算。

代码示例

1
2
3
4
5
6
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum()

stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

阅读全文 »
12
雅客

雅客

一切并非毫无意义

16 日志
6 分类
18 标签
RSS
GitHub
© 2018 雅客
本站访客数:
博客全站共35.1k字