无类型参数
Lambda表达式的基本结构是:
f->{ }
作为参数的变量名,是可以自定义的,不是固定的。Lambda表达式在功能上等同于一个匿名方法:
public void unknown(f){
System.out.println(f.getName());
}
类型识别
f变量的类型是系统根据上下文自动识别的。
List<Fruit>fruits = Arrays.asList(......);
fruits.forEach(f->{
System.out.println(f.getName());
});
forEach()方法表示遍历fruits集合,每次循环时f变量指代当前遍历到的元素。
由于fruits变量的类型使用泛型语法定义为List<Fruit>,表示集合中的元素的类型是Fruit。
所以,f变量的类型自然是Fruit,就可以调用Fruit类的方法了。
演示一下Collections中sort()方法的排序功能。
List<Student> students = new ArrayList<Student>();
students.add(new Student(111, "bbbb", "london"));
students.add(new Student(131, "aaaa", "nyc"));
students.add(new Student(121, "cccc", "jaipur"));
// 实现升序排序
Collections.sort(students, (student1, student2) -> {
// 第一个参数的学号 vs 第二个参数的学号
return student1.getRollNo() - student2.getRollNo();
});
students.forEach(s -> System.out.println(s));
Collections.sort()方法第二个参数是实现匿名类
new Comparator(){}。Lambda表达式又有了一些新写法:多参数箭头(->)前表示参数变量,有多个参数的时候,必须使用小括号包裹:()
(student1,student2)->{}
无参数箭头(->)前表示参数变量,没有参数的时候,必须使用小括号
()->{}
单条执行语句箭头(->)后执行语句只有一条时,可以不加大括号包裹
s->System.out.println(s);
推荐都加上大括号,这样代码的边界明确。
有类型参数
如果代码比较复杂,为了维护和容易阅读,也可以为参数变量指定类型。
fruits.forEach(Fruit f)->{
System.out.println(f.getName());
};
即使只有一个参数,也必须使用小括号包裹,否则会出错。
Arrays.asList()
【1. 要点】
该方法是将数组转化成List集合的方法。
List list = Arrays.asList("a","b","c");
注意:
(1)该方法适用于对象型数据的数组(String、Integer...)
(2)该方法不建议使用于基本数据类型的数组(byte,short,int,long,float,double,boolean)
(3)该方法将数组与List列表链接起来:当更新其一个时,另一个自动更新
(4)不支持add()、remove()、clear()等方法
总结:如果你的List只是用来遍历,就用Arrays.asList()。
如果你的List还要添加或删除元素,还是乖乖地new一个java.util.ArrayList,然后一个一个的添加元素。
【3.示例代码】
package cn.wyc;
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args){
//1、对象类型(String型)的数组数组使用asList(),正常
String[] strings = {"aa", "bb", "cc"};
List<String> stringList = Arrays.asList(strings);
System.out.print("1、String类型数组使用asList(),正常: ");
for(String str : stringList){
System.out.print(str + " ");
}
System.out.println();
//2、对象类型(Integer)的数组使用asList(),正常
Integer[] integers = new Integer[] {1, 2, 3};
List<Integer> integerList = Arrays.asList(integers);
System.out.print("2、对象类型的数组使用asList(),正常: ");
for(int i : integerList){
System.out.print(i + " ");
}
// for(Object o : integerList){
// System.out.print(o + " ");
// }
System.out.println();
//3、基本数据类型的数组使用asList(),出错
int[] ints = new int[]{1, 2, 3};
List intList = Arrays.asList(ints);
System.out.print("3、基本数据类型的数组使用asList(),出错(输出的是一个引用,把ints当成一个元素了):");
for(Object o : intList){
System.out.print(o.toString());
}
System.out.println();
System.out.print(" " + "这样遍历才能正确输出:");
int[] ints1 = (int[]) intList.get(0);
for(int i : ints1){
System.out.print(i + " ");
}
System.out.println();
//4、当更新数组或者List,另一个将自动获得更新
System.out.print("4、当更新数组或者List,另一个将自动获得更新: ");
integerList.set(0, 5);
for(Object o : integerList){
System.out.print(o + " ");
}
for(Object o : integers){
System.out.print (o + " ");
}
System.out.println();
//5、add() remove() 报错
System.out.print("5、add() remove() 报错: ");
// integerList.remove(0);
// integerList.add(3, 4);
// integerList.clear();
}
}
输出:
1、String类型数组使用asList(),正常: aa bb cc
2、对象类型的数组使用asList(),正常: 1 2 3
3、基本数据类型的数组使用asList(),出错(输出的是一个引用,把ints当成一个元素了):[I@1540e19d 这样遍历才能正确输出:1 2 3 4、当更新数组或者List,另一个将自动获得更新: 5 2 3 5 2 3 5、add()、remove()、clear() 报错:
引用外部变量
Lamdba表达式{}内的执行语句,出来能引用参数变量以外,还可以引用外部变量。
List<Fruit>fruits = Arrays.asList(.....);
String message = "水果名称;";
fruits.forEach(f->{
System.out.println(message+f.getName());
});
message = " ";
上面的代码运行后会报错,我们借此来认识一些书写规范:规范一引用的局部变量不允许被修改。
即使写在表达式后面也不行
下面代码修改了局部变量也是错误的:
String message;
fruit.forEack(f->{
message = "水果名称:";
});
Lambda表达式引用的局部变量即使不声明为
final,也要具备final的特性,变量值初始化后不允许被修改。
在表达式外实际赋值一次的写法是允许的:
String message;
message = "水果名称";
规范二
参数不能与局部变量同名,下面的就是错误的:
String f = "水果名称";
fruits.forEach(f->{});
双冒号(::)操作符
前面的例子中,只有一条执行语句的Lambda表达式:
List<String>names = Arrays.asList("zhangSan","LiSi","WangWu");
names.forEach(n->{
System.out.println(n);
});
可以进一步简化为:
names.forEach(System.out::println);
使用::时,系统每次遍历取得的元素(n),会自动作为参数传递给System.out.println()方法打印输出:System.out::println等同于n->{System.out.println(n)}
::语法省略了参数变量
语法含义
双冒号::语法,当然也是不能独立使用的,需要配合特定的方法
不同用法
静态方法调用:使用LambdaTest::println代替f->LambdaTest.print(f)简化了代码
调用非静态方法:方法print()不在标识为static,于是需要实例对象来调用
fruits.forEach(new LambdaTest()::print);
只是简写了:
LambdaTest lt = new LambdaTest();
fruits.forEach(lt::print);
效果是一样的。开头的例子中,System.out::println语句就是调用非静态方法println(),因为System.out代指的是一个实例对象,只是这个实例对象比较复杂。
多参数:
Collection.sort(student,(student1,student2)->{
return student1.getRollNo() - student2.getRollNo();
});
如果吧比较难的过程定义成一个方法:
private static int compute(Student s1,Student s2){
.... ...
}
那么,排序过程就可以简写为:
Collections.sort(students,SortTest::compute);
注意,系统会自动获取上下文的参数,并按上下文定义的顺序传递给指定的方法,所谓顺序就是Lambda表达式()中的顺序。
父类方法super关键字的作用是在子类中引用父类的属性或者方法,那么同样,::语法也可以用super关键字调用父类的非静态方法
import java.util.Arrays;
import java.util.List;
public class LambdaTest extends LambdaExample {
public static void main(String[] args) {
List<Fruit> fruits = Arrays.asList(
new Fruit("香蕉"),
new Fruit("苹果"),
new Fruit("梨子"),
new Fruit("西瓜"),
new Fruit("荔枝")
);
LambdaTest at = new LambdaTest();
at.print(fruits);
}
public void print(List<Fruit> fruits){
fruits.forEach(super::print);
}
}
class LambdaExample {
public void print(Fruit f){
System.out.println(f.getName());
}
}
流迭代
先回顾Spring中内容在Spring Resource中的classpath部分学过,在java内部中,我们一般吧文件路径称为classpath,所以读取内部文件就是从classpath内读取,classpath指定的文件不能解析成File对象,但是可以解析成InputStream,我们借助Java IO就可以读取出来。
使用commons-io这个库
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
代码如下
public class Test{
public static void main(String[] args){
//读取classpath的内容
InputStream in = Test.class.getClassLoader().getResourceAsStream("data.json");
//使用commons-io库读取文本
try{
String content = IOUtils.toString(in,"utf-8");
System.out.println(content);
}catch(IOException e){
e.printStackTrace();
}
}
}
回到流迭代中在Java中,Stream是一个接口,在接口中提供了操作数据的方法,通常我们叫做API。创建流
- 直接创建
import java.util.stream.Stream;
Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
- 由数组转化
String[] fruitArray = new String[] {"苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"};
Stream<String> stream = Stream.of(fruitArray);
1.由集合转化
List<String>fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("哈密瓜");
fruits.add("香蕉");
fruits.add("西瓜");
fruits.add("火龙果");
Stream<String>stream = fruits.stream();
由于原数据(集合或数组)是有序的,所以流中的数据也是有序排列的。
流迭代
Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
stream.forEach(System.out::println);
为了让系统自动识别Lambda表达式的参数类型,也必须使用泛型语法指定
Stream中对象的类型。泛型,即参数化类型也就是把类型当做参数传递,在创建类的对象的时候,可以传入任意类型.
流数据过滤
小学生模型
public class Pupil {
private String name;
// 平均分
private int averageScore;
// 违规次数
private int violationCount;
}
我们要筛选出分数不低于80,且没有违规记录的。
//存入小学生数据对象
List<Pupil>pupils = new ArrayList<>();
//符合条件的小学生集合
List<Pupil>qualified = new ArrayList<>();
for(Pupil pupil:pupils){
if (pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1) {
qualified.add(pupil);
}
}
for(Pupil pupil:qualified){
System.out.println(pupil.getName());
}
另一种写法我们使用Java8新特性来完成。
List<Pupil>pupils = new ArrayList<>();
pupils.add(new Pupil());
pupils.stream()
.filter(pupil -> pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1)
.forEach(pupil->{System.out.println(pupil.getName());});
filter()方法从方法名我们可以理解:对流中的数据对象进行过滤
方法参数是一个Lambda表达式,箭头后面式条件语句,判断数据需要符合的条件。
也就是说,使用Lambda表达式告诉过滤器,需要哪些符合条件的数据
等同于
pupil ->((pupil.getAverageScore()>=80)&&(pupil.getViolationCount()<1))
下面我们来对100个随机整数,使用Stream API和Lambda表达式判断哪些式偶数并输出这些偶数
public static void main(String[] args) {
List<Integer> evenNumbers = initNumbers();
evenNumbers.stream()
.filter(evenNumber->evenNumber%2==0)
.forEach(System.out::println);
}
private static List<Integer> initNumbers() {
List<Integer> evenNumbers = new ArrayList<>();
int i = 1;
while (i <= 100) {
evenNumbers.add((int)(Math.random() * 101));
i++;
}
return evenNumbers;
}
流的设计思想
数据流的操作过程,可以看作一个管道,管道由多个节点组成,每个节点完成一个操作。
.filter().forEach()组成了一个管道,每个方法都是管道的一个节点。方法调用的顺序构成了管道节点顺序。相比普通java代码,Stream的显著特点是:编程的重点,不再是对象的运用,而是数据的计算。函数式风格,
数据流映射
小习题
List<Integer>numbers = Arrays.asList(3,2,2,7,63,2,4,5);
计算每个数字的平方并输出。
numbers.stream()
.map(num->{
return num*num;
})
.forEach(System.out::println);
这里用到的是map()方法
map()方法统称为映射,其作用就是用新的元素替换掉流中原来相同位置的元素,相当于每个对象都经历一次转换。
映射到新的数据map()方法的参数是一个Lambda表达式,在语句块中对流中的每个数据对象进行计算,处理,最后用return语句返回的对象,就是转换后的对象。
映射后的对象类型,可以与流中原始的对象类型不一致。在流中,可以用字符串替换原来的整数,这就极大的提供了灵活性、扩展性,让流的后继操作可以更加方便。
少数情况下,如果替换语句简单,系统能自动识别需要返回的值,代码可以简写为:
.map(num->num*num)
流数据排序
我们学会了使用Lambda表达式优化语句:
List<Student> students = new ArrayList<Student>();
students.add(new Student(111, "bbbb", "london"));
students.add(new Student(131, "aaaa", "nyc"));
students.add(new Student(121, "cccc", "jaipur"));
//实现升序排序
Collections.sort(students,(student1,student2)->{
return student1.getRollNo()-student2.getRollNo();
});
students.forEach(s->System.out.println(s));
使用Stream API更简单
students.stream()
.sort((student1,stduent2)->{
return student1.getRollNo()-student2.getRollNo();
})
.forEach(System.out::println);
sorted()顾名思义,就是完成排序的方法。把排序规则写成一个Lambda表达式传给此方法即可。
参数顺序student1代指后一个元素,student2代指前一个元素,
第一次遍历时,student1指代第二个名为aaaa的学生,student2指代第一个名称为bbbb的学生。
如果计算语句简单,可以简写为
.sorted((student1,student2)->student1.getRollNo() - student2.getRollNo());
不论是集合排序Collections.sort()还是流排序sorted()都需要返回以个数值。返回非正数就表示两个相比较的元素需要交换位置,返回正数就不需要。
流数据摘取
numbers.stream()
.sorted((n1,n2)->n2-n1)
.limit(3)
.forEach(System.out::println);
limit()方法的作用是返回流的前n个元素,n不能为负。
不能摘取任意位置,只能是流开头的。
流合并
filter、map、sorted都统称为聚合操作聚合操作就是把集合中的对象做整体性的计算。
对1-10的十个正整数求和
List<Integer>numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
普通的使用for循环完成计算的代码是:
int sum = 0;
for(int i:numbers){
sum+=i;
}
System.out.println("sum:"+sum);
使用Stream API完成计算的代码:
import java.util.Arrays;
int sum = numbers.stream()
.reduce((a,b)->a+b)
.get();
System.out.println("1-10求和 : " + sum);
reduce()方法的作用,是合并了所有的元素,终止计算出一个结果,注意这里终止的意思,就是流已经达到终点结束了,不能再继续流动了。
forEach()也是流的终点。
reduce()方法返回值需要调用get()方法返回最终的整数值。
同理get()方法返回值的类型,也是系统自动根据流中元素的类型推定的。
reduce()的参数:
- a再第一次执行运算语句a+b时,指代流的第一个元素,然后充当缓存作用以存放本次计算结果,此后执行计算语句时,a的值就是上一次的计算结果并继续充当缓存存放本次计算结果。
- b参数第一次执行计算语句时指代流的第二个元素,此后一次指代流的每个元素。
ab两个参数的作用时由位置决定的,变量名时任意的。
数据如下:
List<Student> students = new ArrayList<>();
students.add(new Student("赵祯", 92));
students.add(new Student("曹丹姝", 60));
... ...
... ...
计算学生分数。
Student result = students.stream()
.reduce(new Student("", 0),
(a, b) -> {
a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
return a;
}
);
System.out.println(result.getName() + " - " + result.getMidtermScore());
-
第一个参数,是作为缓存角色的对象
-
第二个参数是lambda表达式,
- 那么a变量不再指代流中的第一个元素了,专门指代缓存角色的对象,即方法第一个参数对象。
- b变量依次指代流的每个元素,包括第一个元素。
reduce()方法返回作为缓存角色的对象,即第一个参数。
流收集
forEach()方法和reduce()方法都是流的终点。
List<Integer>numbers = Array.asList(3,2,2,7,63,2,3,5);
找出最大的前三个数字,放到一个新的集合中,用-组合成字符串打印
import java.util.stream.Collectors;
List<String>numberResult = numbers.stream()
.sorted((n1,n2)->n2-n1)
.limit(3)
.map(a->""+a)
.collect((Collectors.toList()));
String string = String.join("."+numResult);
System.out.println("字符串是: " + string);
collect()方法的作用就是收集元素,Collectors.toList()是一个静态方法,作为参数告诉collect()方法存入一个List集合,所以collect()方法的返回值类型就是List为了能够把最终结果转换为字符串打印,调用了map()方法把流中原来的整数映射为字符串(""+a),所以collect()方法的返回值类型就是List,而不是List
并行流
为了发挥多核CPU的优势,将串行计算模式改为并行计算模式(多线程,同时执行)。
使用并行流的代码很简单,不在调用stream()方法,改为调用paralleStream()方法即可。
设计模式
单例模式
保证一个类仅有一个实例,要做到这一点,核心办法就是把构造函数设置为私有的。
public class ClassMaster {
private String id;
// 班主任名称
private String name;
private String gender;
private ClassMaster() {
}
}
ClassMaster可以自己实例化自己
public class ClassMaster {
private String id;
// 班主任名称
private String name;
private String gender;
// 唯一实例
private static ClassMaster instance = new ClassMaster();
private ClassMaster() {
}
}
必须使用
static修饰符,否则会造成死递归
也就是说,不允许其他类实例化ClassMaster(私有构造方法)、只有自己能实例化一个唯一的自己(private static),所以可以保证ClassMaster的实例是全局唯一的。
这种可以保证只有一个实例对象的方式,就是单例设计模式
访问实例类new出一个实例的目的就是要给其他的类使用。所以还需要增加一个方法,允许其他类访问这个单例的实例。
public class ClassMaster {
private String id;
// 班主任名称
private String name;
private String gender;
// 唯一实例
private static ClassMaster instance = new ClassMaster();
private ClassMaster() {
}
// 外部类可以通过这个方法访问唯一的实例
public static ClassMaster getInstance() {
return instance;
}
}
从Console可以看出,虽然访问了多次班主任,但构造函数只执行了一次。Spring中的单例类变量使用@Autowired注解,能够自动注入实例对象。实际上,任何自动注入实例对象,默认只有一个实例对象,是单例的,例如:可能多个Service和Control等都需要用到用户服务,那么这些类中都会定义:
@Autowired
private UserService userService;
Spring会保证只生成一个UserServiceImpl实例,注入到多个Service和Control中,不会为每个Service和Control分别new出多个userserviceimpl实现类的实例
简单工厂模式
这里的工厂就是生产实例对象的地方。
这种模式可以解决代码重复和耦合紧密(当西瓜过季了,甜水果店需要由西瓜改为凤梨时,餐厅,水果超市,甜品点等都需要改。如果由几百个店铺卖水果,那就是灾难)
所谓耦合,指的是某个类的代码,包含了其他类的逻辑细节。包含的越多,耦合越紧密,也越容易相互影响。甜水果需要由西瓜改为凤梨时,影响了所有的水果店,就是耦合紧密,耦合度高。
简单工厂模式,需要两个步骤:
- 从具体的产品类抽象出接口,意味着工厂应该生产书出一种产品,不应该生产某一个产品。
public class FruitFactory {
public static Fruit getFruit(Customer customer) {
Fruit fruit = null;
if ("sweet".equals(customer.getFlavor())) {
fruit = new Watermelon();
} else if ("acid".equals(customer.getFlavor())) {
fruit = new Lemon();
} else if ("smelly".equals(customer.getFlavor())) {
fruit = new Durian();
}
return fruit;
}
}
工厂仍然需要实现功能,完成“根据不同的条件创建不同对象”需求。
抽象工厂模式
需要创建一个系列,多种产品的时候,简单工厂模式就不太适用了。
对于一批、多种类型的对象需要创建的场景,使用抽象工厂模式会更好。简单工厂模式的主要作用就是把多个产品抽象,使用一个工厂统一创建,那么抽象工厂模式就是把多个工厂也进一步抽象。
咋一看很复杂,但仔细分析,实际上就是进一步抽象出来工厂接口(SnacksFactory),然后多了一个SnackSFactoryBuilder
- 工厂接口工厂接口
SnacksFactory即规定工厂应该提供什么样的产品,所以包含了所有工厂的方法。
public interface SnacksFactory {
// 取得水果
public Fruit getFruit(Customer customer);
// 取得饮料
public Drink getDrink(Customer customer);
}
但有一个问题,水果工厂是不提供饮料的,但是水果工厂实现工厂接口后,又必须实现getDrink()方法,这时候直接返回null即可。
public class FruitFactory implements SnacksFactory {
public Fruit getFruit(Customer customer) {
Fruit fruit = null;
if ("sweet".equals(customer.getFlavor())) {
fruit = new Watermelon();
} else if ("acid".equals(customer.getFlavor())) {
fruit = new Lemon();
} else if ("smelly".equals(customer.getFlavor())) {
fruit = new Durian();
}
return fruit;
}
public Drink getDrink(Customer customer) {
return null;
}
}
水果工厂不真正实现
getDrink()方法,只是基于接口的需要,给一个没有实际作用的方法实现。
- 工厂的厂
SancksFactoryBuilder称之为生产工厂的厂,工厂用来生成产品实例,SnacksFactoryBuilder用来生成工厂实例
public class SnacksFactoryBuilder {
public SnacksFactory buildFactory(String choice) {
if (choice.equalsIgnoreCase("fruit")) {
return new FruitFactory();
} else if (choice.equalsIgnoreCase("drink")) {
return new DrinkFactory();
}
return null;
}
}
注意:与简单工厂不同的是:SnacksFactoryBuilder的buildFactory()方法并不是static的。因为复杂场景下尽量不要使用类(static)方法,实例方法可以被继承,扩展性好,应该优先使用实例方法。
工厂模式结合Spring工程
不提倡在工厂中定义static方法的另一种原因是:在使用Spring框架的时候,可以为SnacksFactoryBuilder加上@Component注解,可以让框架管理实例:
@Component
public class SnacksFactoryBuilder{
public SnacksFactory buildFactory(String choice){
}
}
简单工厂模式的工厂类也可以像这样去掉static、加注解。
相应的,任何需要使用工厂的地方,只需要使用@Autowired注解让框架自动注入实例即可:
@Service
public class XxxxxServiceImpl implements XxxxxService {
@Autowired
private SnacksFactoryBuilder snacksFactoryBuilder;
}
这样可以让工厂模式的代码与Spring互为一体,扩展性更好、易于维护。
观察者模式
基本思路提供一个天气服务器,谁想了解天气信息,从这个服务端订阅就好了:当天气发生变化,自动通知每个客户端。
这种订阅/通知的场景,很适合观察者模式来实现。
- 观察什么
什么对象发生变化了需要发出通知。天气对象发生变化了需要发送通知。
import java.util.Observable;
public class WeatherData extends Observable {
// 城市
private String cityName;
// 时间
private String time;
// 温度
private String temp;
// 城市固定了就不变了
public WeatherData(String cityName) {
this.cityName = cityName;
}
// 打印天气信息
public String toString() {
return cityName + "," + LocalDate.now().toString() + " " + time + ",气温:" + temp + "摄氏度。";
}
public String getCityName() {
return cityName;
}
public String getTime() {
return time;
}
public String getTemp() {
return temp;
}
}
天气信息类WeatherData继承了Observable类。Observable类是Java提供的,继承了就表示是核心的,需要被观察的类。
记住这个写法:extends Observable
这个设计与以前模型设计不同的是,一个WeatherData代表一个城市的天气,初始化完毕以后就不能改了。所以去掉了所有熟悉的setter方法。WeatherData是被观察者。
- 数据变化后发起通知
时间和气温是监听的终点信息,所以增加一个新的方法。
import java.util.Observable;
public class WeatherData extends Observable {
/**
* 一个城市的气温在某个时刻发生了变化
*/
public void changeTemp(String time, String temp) {
if(time == null || temp == null) {
// 输入数据为空是有问题的,不处理
return;
}
// 与原数据不同,说明发生了变化
if(!time.equals(this.time) || !temp.equals(this.temp)) {
// 标记变化
super.setChanged();
this.time = time;
this.temp = temp;
// 发出通知,参数是额外的信息
super.notifyObservers("温度变化已通知");
}
}
}
在changeTemp()中,如果天气数据与原来的不同,则会标记变化并发出通知。父类Observable提供的方法setChanged()就是标记被观察者对象发送了变化。父类Observable提供的方法notifyObservers()就是发出通知;如果需要发送额外(不在被观察者对象里面的)的信息,在参数中传入信息对象,可以是任意对象,需要自己根据具体的需求场景而定。这里新增一个changeTemp()方法的主要原因是,天气信息有多个字段组成,所以最好有一个统一的变更数据的方法,防止混乱。如果只有一个数据属性,直接把发通知的这段逻辑写在setter方法里也可以。
- 谁接收通知需要了解天气的类,就是接收通知的类,通常叫做观察者。观察者需要实现
Observer接口,也是Java提供的,实现此接口表示作为观察者。
import java.util.Observable;
import java.util.Observer;
public class WeatherObserver implements Observer {
private String name;
@Override
public void update(Observable o, Object arg) {
System.out.print(this.name + "观察到天气变化为:");
System.out.print(o.toString());
System.out.println(" " + arg);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
作为观察者,实现Observer接口后,要自己实现update()方法,方法签名是接口中定义好的,属于固定写法。
- 第一个参数就是被观察者对象,被观察者对象都需要继承自Observable
- 第二个参数就是额外的信息,具体说就是调用
super.notifyObservers()是传入的参数对象;传入什么对象,arg的值就是什么对象。
如果这里不想发送额外信息,写为super.notifyObservers(null),那么这里的arg值就是null,注意避免空指针异常。
update()方法的作用就是接收通知。实际上,系统在super.notifyObservers()发出通知后,及调用所有观察者的update()方法,完成通知的过程。
- 运行当天气发生变化时,只需要调用
changeTemp()方法即可改变天气数据。
public class WeatherTest {
public static void main(String[] args) {
// 在天气变化后发邮件的观察者
WeatherObserver w1 = new WeatherObserver();
w1.setName("天气邮件观察者");
// 在天气变化后发短信的观察者
WeatherObserver w2 = new WeatherObserver();
w2.setName("天气短信观察者");
// 城市天气数据
WeatherData weatherData = new WeatherData("余杭");
// 添加观察者
weatherData.addObserver(w1);
weatherData.addObserver(w2);
// 气温变化
weatherData.changeTemp("11:08", "32.8");
// 气温变化
weatherData.changeTemp("14:46", "29.3");
}
}
观察者可以有多个。观察者对象与被观察者对象谁先new出来都可以,但是必须先调用addObserver()方法把观察者对象实例添加到被观察者(天气数据)实例中,然后再调用自定义的changeTemp()方法变更天气,才能触发自动通知。实现观察者模式,主要学会Observable父类和Observer接口提供的几个方法。
跟工厂模式不同的是,观察者模式主要描述的是类的行为,而不是如何创建。跟工厂模式的思想相同的是,观察者模式让观察者和被观察者双方的耦合度降到最低(称之为解耦)
观察者不需要知道数据变化后需要通知给谁,发出通知即可;而且不需要知道谁收到通知了谁没收到,由系统保证。
并发编程
继承Thread类
- 线程类可以继承java的
Thread类实现线程类
public class Person extends Thread{
@Override
public void run(){
try{
System.out.println(getName()+"开始取钱");
Thread.sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(getName()+"取钱完毕");
}
}
继承Thread类之后需要重写父类的run()方法,注意必须修饰为public void,方法是没有参数的。
加上
@Override注解,会让系统自动检查public void run()方法定义有没有写错
线程类的作用就是完成一段相对独立的任务。这里用Thread.sleep(200)模拟取钱的过程,sleep()方法(静态方法)的作用就是让线程睡眠200毫秒。
- 运行线程
线程需要调用start()方法才可以启动
public class Bank {
public static void main(String[] args) {
Person thread1 = new Person();
thread1.setName("张三");
Person thread2 = new Person();
thread2.setName("李四");
thread1.start();
thread2.start();
}
}
Thread父类中由name属性,但是private的,所以可以调用setName()方法为线程设置名字,通过getName()就知道是那个线程在运行。
根据具体的要求,线程类也可以增加更多其他属性。只要不遗漏run()方法就行。
我们需要了解的是,线程类的run()方法是系统调用start()后自动执行的,编程时不需要调用run()方法。但永远无法知道系统在什么时刻调用,是立刻调用,还是延迟一小时调用,都由系统决定。