感想:这门课难度和任务量都非常大,第一次接触java的过程也非常痛苦,课件还是英文的,学的很难受。
以下为知识点总结。
一二。
1.软件构造的多维度视图
按阶段划分:构造时/运行时视图
按动态性划分:时刻/阶段(形态)视图
按构造对象的层次划分:代码(逻辑)/构件(物理)视图
2.阶段划分、各阶段活动
时刻 | 阶段 | |||
代码 | 构件 | 代码 | 构件 | |
编译时 | 源代码、抽象语法树、类和接口的属性、方法 | 包、文件、静态链接、库、测试用例、编译脚本 | 代码改动 | 配置项、版本控制 |
运行时 | 代码快照、堆快照 | 包、库、动态链接、配置(加载时)、数据库、中间件、硬件 | 异常栈轨迹、多线程同步、执行跟踪(代码层面) | 事件日志、多进程、分布式进程(构件、系统层面) |
过程调用图、消息图 |
3.软件质量指标
(1)外部指标(影响用户)
①正确性
按照预先定义的“规约”执行,至高无上的质量指标。
需要在软件系统的每一层保证自己的正确性,同时假设其下层是正确的。
通过测试和调试、防御式编程、形式化方法等手段保证正确性
②健壮性
针对异常情况(取决于spec的范畴)的处理,出现异常时不要“崩溃”。
出现规约定义之外的情形的时候,软件要做出恰当的反应,是对正确性的补充。
未被specification覆盖的情况即为“异常情况”
③可扩展性
对软件的规约进行修改,是否足够容易?
两种原则:简约主义设计,分离主义设计
④可复用性
发现共性,一次开发,多 次使用
不要不断重复自己写过的东西,不要重复造轮子
⑤兼容性
不同的软件系统之间相互可容易的集成
需要保证设计的同构性:
-标准统一的文件格式
-标准统一的数据结构
-标准统一的用户接口
-标准统一的访问协议
⑥性能
性能是软件系统对硬件资源尽可能少地提出要求的能力,例如处理器时间、内部和外部存储器占用的空间、通信设备中使用的带宽。
性能毫无意义,除非有足够的正确性
对性能的关注要与其他质量属性进行折中
过度的优化导致软件不再适应变化和复用
⑦可移植性
软件可方便的在不同的技术环境(硬件、操作系统)之间移植'
⑧易用性
容易学、安装、操作、监控
给用户提供详细的指南
结构简单,了解用户。
⑨功能性
功能性是系统提供的可能性的程度。
程序设计中一种不适宜的趋 势,即软件开发者增加越来越多的功能,企图跟上竞争,其结果是程 序极为复杂、不灵活、占用过多的磁盘空间
每增加一小点功能,都确保其他质量属性 不受到损失
⑩及时性
软件系统在用户需要时或之前发布的能力。
(2)内部指标(影响软件本身和它的开发者,外部质量取决于内部质量)
源代码相关因素,如代码行数(loc)、循环复杂性等
体系结构相关因素,如耦合、内聚等
可读性
可理解性
清晰度
大小
(3)指标折中
正确的软件开发过程中,开发者应该将不同质量因素之间如何做出折中的设计决策和标准明确的写下来
虽然需要折中,但“正确性”绝不能与其他质量因素折中。
最重要的几个质量因素:正确性、健壮性(可靠),复用性、可扩展性(模块化)
三。
1.基本数据类型、引用数据类型
基本数据类型
1、在基本数据类型中,除了boolean类型所占长度与平台有关外,其他数据类型长度都是与平台无关的。比如,int永远占4个字节(1 Byte = 8 bit)。
2、void不是基本数据类型
3、基本数据类型的默认值仅在作为类中属性时生效,在方法内部必须先对基本数据类型变量赋值后才能使用,否则编译不通过。
引用数据类型
引用类型(reference type)指向一个对象,不是原始值,指向对象的变量是引用变量。
在java里面除去基本数据类型的其它类型都是引用数据类型,自己定义的class类都是引用类型,可以像基本类型一样使用。
引用类型常见的有:String,StringBuffer,ArrayList,HashSet,HashMap等。
如果要对比两个对象是否相同,则需要使用equals()方法,但有一点需要注意:equals()方法的默认行为是比较引用。如果是你自己写的类,你应该为它重写equals()来比较对象的内容。大多数Java类库都实现了比较对象内容的equals()方法。
两者比较
2.类型检查
静态类型检查:编译时可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
关于“类型”的检查,不考虑值
①语法错误
②类名、函数名错误
③参数数目错误
④参数类型错误
⑤返回值类型错误
⑥其他情况,如变量可能没有初始化,在一些条件分支里声明的变量无法在分支外使用等等
动态类型检查:运行时
关于“值”的检查
①非法的参数值,如除零错误
②非法的返回值,如返回的具体值不能被转换成对应的类型
③越界
④空指针
3.可变对象、不可变对象、final关键字
不变对象:
如String,一旦被创建,始终指向同一个值/引用。如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。
不提供可改变其内部数据的值的操作。
可变对象:
如StringBuilder,拥有方法可以修改自己的值/引用。
提供了可改变其内部数据的值的操作。
比较:
①使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收) ,使用可变类型会减少拷贝
②使用可变数据类型,可以提高性能,方便在多个模块之间共享数据。
③使用不可变数据类型,更容易达到安全要求。
final关键字:
①修饰类当用final去修饰一个类的时候,表示这个类不能被继承。
注意:
a. 被final修饰的类,final类中的成员变量可以根据自己的实际需要设计为fianl。
b. final类中的成员方法都会被隐式的指定为final方法。
②被final修饰的方法不能被重写。
注意:
a. 一个类的private方法会隐式的被指定为final方法。
b. 如果父类中有final修饰的方法,那么子类不能去重写。
③修饰局部变量
注意:
a. 必须要赋初始值,而且是只能初始化一次。
④修饰成员变量
注意:
a. 必须初始化值。
b. 被fianl修饰的成员变量赋值,有两种方式:1、直接赋值 2、全部在构造方法中赋初值。
c. 如果修饰的成员变量是基本类型,则表示这个变量的值不能改变。
d. 如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变
4.防御式拷贝
返回可变类型对象时,为了防止该对象在别处被修改,创建一个新的对象,并复制原来的对象的各项属性,并返回这个复制后的新对象。
5.snapshot diagram
①基本类型的值
②对象类型的值
③不可变对象:双线椭圆
④可变对象
⑤不可变的引用:双线箭头
6.spec、precondition、postcondition
spec:
写在方法之前,只讨论输入输出的数据类型、功能和正确性、性能等,不讨论具体实现
应该包含前置条件、后置条件、和函数期望完成的行为。
@param 输入参数的含义
@return 返回参数的含义
@throws 抛出异常的含义
前置条件:
对客户端的约束,在使用方法时必须满足的条件
后置条件:
对开发者的约束,方法结束时必须满足的条件
前置条件满足,则后置条件必须满足。
前置条件不满足,则方法可做任何事情。
在涉及到可变类型等情况时,不要只依靠客户端和开发者的行为,要在规约里限定住。
7.行为等价性
定义:两个函数是否可相互替换?
①单纯的看实现代码,并不足以判定不同的implmentation是否是“行为等价的”
②需要根据spec判定行为等价性
③在编写代码之前先要确定spec如何形成、撰写。
8.spec强度
spec变强:更放松的前置条件,更严格的后置条件。
意味着实现更难,使用更轻松。
spec强的函数可以替换spec弱的。
9.ADT的四种基本操作
①构造器:t* -> T
创建对象,可能为静态函数或构造函数
②变值器:T+,t* -> void|t|T
通常返回void,但改变了内部的某些状态。
也可能返回非空类型。
③观察器:T+,t* -> t
返回内部某些状态
④生产器:T+,t* -> T
从旧对象中构建新对象。
10.表示独立性
内部实现如何变化,不影响客户端使用,也不影响规约。
如果没写前置、后置条件,就不能改变内部表示。
11.AF、RI
RI:
不变量,程序的某种“特性”,这种特性无论在什么时候都需要成立。
表示不变量,表示空间集合中的一个子集,包含了所有合法的表示值,或者说对于合法表示的描述。
和客户端无关,由ADT自身负责维持不变量。
如果出现了表示泄露,就不能保证不变量,也不能保证独立性。
ADT要保证不变性、不产生表示泄露。
checkRep()表示检查表示不变量的函数,要保证ADT的四个操作都要执行这个检查函数,并且在"null"的情况下不能通过检查。
R:表示空间,实际存入ADT中真实的值
A:抽象空间,使用ADT的客户端看到和使用的值。
AF:R→A的一个映射关系,是R中的值在A中的解释。
这个映射是满射,但未必是单射、双射,并且R中的一些值可能是非法的,在A中无对应。
同样的R、RI,但可能有不同的AF,即解释不同。
12.表示泄露
把R空间的东西泄露给了客户端
一旦泄露(将引用传递到客户端),内部表示可能被意外更改,无法保证RI
13.接口、抽象类、类
14.访问控制符:public,protected,default,private
①私有权限 private
private可以修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类,不考虑内部类)。被private修饰的成员,只能在定义它们的类中使用,在 其他类中不能调用。
②默认权限 (default)
类,数据成员,构造方法,方法成员,都能够使用默认权限,即不写任何关键字。默认权限即同包权限,同包权限的元素只能在定义它们的类中,以及同包 的类中被调用。
③受保护权限protected
protected可以修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类,不考虑内部类)。被protected修饰的成员,能在定义它们的类中,同包(最后一次重写该函数的位置)的类中被调用。如果有不同包的类想调用它们,那么这个类必须是定义它们的类的子孙类。
④公共权限 public
public可以修饰类,数据成员,构造方法,方法成员。被public修饰的成员 ,可以在任何一个类中被调用,不管同包或不同包,是权限最大的一个修饰符。
有关Java语言的修饰符,需要注意的问题有如下几个:
①并不是每个修饰符都可以修饰类(指外部类),只有public和default可 以。
②所有修饰符都可以修饰数据成员,方法成员,构造方法。
③为了代码安全起见,修饰符不要尽量使用权限大的,而是适用即可。比如 ,数据成员,如果没有特殊需要,尽可能用private。
④修饰符修饰的是“被访问”的权限。
15.重写(override),重载(overload)
16.泛型
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
①泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic<T>{ //key这个成员变量的类型为T,T的类型由外部指定 private T key; public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 this.key = key; } public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 return key; } } //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型 //传入的实参类型需与泛型的类型参数类型相同,即为Integer. Generic<Integer> genericInteger = new Generic<Integer>(123456); //传入的实参类型需与泛型的类型参数类型相同,即为String. Generic<String> genericString = new Generic<String>("key_vlaue"); Log.d("泛型测试","key is " + genericInteger.getKey()); Log.d("泛型测试","key is " + genericString.getKey()); /*在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。*/ Generic generic = new Generic("111111"); Generic generic1 = new Generic(4444); Generic generic2 = new Generic(55.55); Generic generic3 = new Generic(false); Log.d("泛型测试","key is " + generic.getKey()); Log.d("泛型测试","key is " + generic1.getKey()); Log.d("泛型测试","key is " + generic2.getKey()); Log.d("泛型测试","key is " + generic3.getKey());
②泛型接口
//定义一个泛型接口 public interface Generator<T> { public T next(); } /** * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 * 即:class FruitGenerator<T> implements Generator<T>{ * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" */ class FruitGenerator<T> implements Generator<T>{ @Override public T next() { return null; } } /** * 传入泛型实参时: * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T> * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。 */ public class FruitGenerator implements Generator<String> { private String[] fruits = new String[]{"Apple", "Banana", "Pear"}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; } }
③泛型方法
/** * 泛型方法的基本介绍 * @param tClass 传入的泛型实参 * @return T 返回值为T类型 * 说明: * 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。 * 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 * 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 */ public <T> T genericMethod(Class<T> tClass)throws InstantiationException , IllegalAccessException{ T instance = tClass.newInstance(); return instance; } public class StaticGenerator<T> { .... .... /** * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如:public static void show(T t){..},此时编译器会提示错误信息: "StaticGenerator cannot be refrenced from static context" */ public static <T> void show(T t){ } }
17.==,equals(),hashcode()
对于基本数据类型,直接比较值是否相等
对于非基本数据类型,如果未重写equals(),则==和equals()都是直接返回地址,hashcode()默认实现也是直接返回地址。
java内部的一些实现,如list里的contains,在判断相等的时候,使用的是equals()函数,而不是==
java内部的一些依靠哈希表实现的数据结构中,先用hashcode()缩小范围,再用equals()判断相等,所以重写equals()时必须重写hashcode()
Java 语言规范要求 equals 方法具有下面的特性:
1 ) 自反性: 对于任何非空引用 x, x.equals(x)应该返回 true
2 ) 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true, x.equals(y) 也应该返回 true。
3 ) 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返 N true, y.equals(z) 返回 true,x.equals(z) 也应该返回 true。
4 ) 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.eqimIS(y) 应该返回同样的结果。
5 ) 对于任意非空引用 x, x.equals(null) 应该返回 false,
18.可变、不可变对象的等价性
可变对象:引用等价性、对象等价性
引用等价性就是只有内存地址相等才等价
对象等价性就是对象的各个属性相等才等价
比如默认的equals()就是引用等价性。
不可变对象:行为等价性,观察等价性
观察等价性:在不改变状态的前提下,调用观察函数所获得的结果一致。
行为等价性:调用任何方法都显示出一致的结果。大部分情况下是指“同一个对象”。
java中的集合类使用观察等价性,即比较大小、各个元素是否相等,而Stringbuilder比较的是地址,即行为等价性。
如果是观察等价性,那把一个可变对象改变了,他就很可能跟以前不相等了,比如hashset中把一个list改了,再调用contains可能就会有问题。
如果要弄一个观察等价性的判断,最好单独实现一个函数而不要用equals()
四五。
1.里氏替换原则
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。
类型替换:
①子类型只能增加而不能删除方法
②子类型需要实现抽象类中所有未实现的方法
③子类型中重写的方法,它的返回值必须是相等类型或是协变类型。
④子类型中重写的方法,它的返回值必须是相等类型或是逆变类型(java不支持)。
⑤子类型中重写的方法不能抛出额外的异常(必须是被重写方法抛出的异常及其子类)。
方法重写:
①更强的不变量(规则限制)
②更弱的前置条件
③更强的后置条件
2.协变、逆变
协变:子类型代替父类型出现,更具体
比如数组中相应子类型的数组引用可以传给父类型
比如泛型中的通配符<?> <? extends xxx>
逆变:父类型代替子类型出现,更抽象
3.委托
一个对象请求另一个对象的功能
与继承相比,主要发生在对象层面,可以选择性的使用方法,比继承更自由。
①将其传入一个具体方法的参数,用完就解除关系。
②将其写入构造函数的参数中,并设立一个字段与委托对象建立联系。
③直接将其固定在一个字段中。
④将其写入一个字段中,并可通过set()方法更改。
4.黑盒框架、白盒框架
白盒框架:通过继承框架类,重写框架方法使用框架
- 允许扩展每一个非私有方法
- 需要理解父类的实现
- 一次只进行一次扩展
- 通常被认为是开发者框架
黑盒框架:通过委派、组合、调用方法,使用框架类,内部不可见。
- 允许在接口中对public方法扩展
- 只需要理解接口
- 通常提供更多的模块
- 通常被认为是终端用户框架,平台
5.常用接口、功能类总结
Iterator、Iterable
iterator是一个接口
package java.util; public interface Iterator<AnyType> { boolean hasNext(); AnyType next(); void remove(); //上次next执行结果的元素 }
iterable是一个泛型接口
package java.lang; public interface Iterable<AnyType> { Iterator<AnyType> iterator(); }
删除元素的三种方式
1.普通for循环
2.迭代器删除
Iterator<String> it = list.iterator(); while(it.hasNext()) { if("b".equals(it.next())) { //list.remove("b"); //不能用集合的删除方法,因为迭代过程中如果集合修改会出现并发修改异常 it.remove(); } } for(Iterator<String> it2 = list.iterator(); it2.hasNext();) { if("b".equals(it2.next())) { //list.remove("b"); //不能用集合的删除方法,因为迭代过程中如果集合修改会出现并发修改异常 it2.remove(); } }
3.另一种for循环
for (String string : list) { if("b".equals(string)){ list.remove("b"); } }
Observer、Observable
Observer:接口、观察者
package java.util; public interface Observer { void update(Observable o, Object arg); }
Observable:类、被观察者
package java.util; public class Observable { private boolean changed = false; //是否改变状态 private Vector obs; //Vector利用同步方法来线程安全,线程安全在多线程情况下不会造成数据混乱 public Observable() { obs = new Vector(); } public synchronized void addObserver(Observer o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.addElement(o); } } public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } public void notifyObservers() { notifyObservers(null); } public void notifyObservers(Object arg) { Object[] arrLocal; synchronized (this) { if (!changed) //状态值未改变时返回,不通知 return; arrLocal = obs.toArray(); //将Vector转换成数组 clearChanged(); //重置状态 } for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } public synchronized void deleteObservers() { obs.removeAllElements(); } protected synchronized void setChanged() { changed = true; } protected synchronized void clearChanged() { changed = false; } public synchronized boolean hasChanged() { return changed; } public synchronized int countObservers() { return obs.size(); } }
Comparable、Comparator
Comparable是排序接口。若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。
此外,实现此接口的对象可以用作有序映射中的键或有序集合中的集合,无需指定比较器。该接口定义如下:
package java.lang; import java.util.*; public interface Comparable<T> { public int compareTo(T o); }
Comparator是比较接口,我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator接口即可。也就是说,我们可以通过实现Comparator来新建一个比较器,然后通过这个比较器对类进行排序。该接口定义如下:
package java.util; public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }
注意:1、若一个类要实现Comparator接口:它一定要实现compare(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。
2、int compare(T o1, T o2) 是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”。
排序
数组排序:Array.sort(int[] a)
集合排序:
- Collections.sort(List list)
- Collections.sort(List list,Comparator c)
六七
1.可维护性的度量指标
圈复杂度:代码控制流执行路径的数量。
代码行数。
可维护性指数:根据循环复杂度、代码行数、注释密度等计算的一个指数。
继承的层数。
类之间的耦合度。
单元测试覆盖度。
2.内聚、耦合
内聚,指的是同一个模块之间的相互联系
耦合,指的是不同模块之间的相互联系
高内聚、低耦合,指的是各个模块之间的独立性要强,模块内部的相关性要强、尽量只负责一项任务。
3.正则语法、正则表达式
首先要注意,“\”这个字符在java里是转义字符,在正则语言里也是转义字符,这就导致了“\”是java转义字符,"\\"是java斜杠、java中正则语言的转义字符,"\\\\"才是java正则语言的斜杠。
java.util.regex 包主要包括以下三个类:
- Pattern 类:
pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。
- Matcher 类:
Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。
- PatternSyntaxException:
PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。
Matcher方法:
分组匹配示例
Pattern p=Pattern.compile("([a-z]+)(\\d+)"); Matcher m=p.matcher("aaa2223bb"); m.find(); //匹配aaa2223 m.groupCount(); //返回2,因为有2组 m.start(1); //返回0 返回第一组匹配到的子字符串在字符串中的索引号 m.start(2); //返回3 m.end(1); //返回3 返回第一组匹配到的子字符串的最后一个字符在字符串中的索引位置. m.end(2); //返回7 m.group(1); //返回aaa,返回第一组匹配到的子字符串 m.group(2); //返回2223,返回第二组匹配到的子字符串
找到所有的匹配串
Pattern p=Pattern.compile("\\d+"); Matcher m=p.matcher("hit117mid116fit11111"); while(m.find()) { System.out.println(m.group()); }
4.健壮性、正确性
健壮性:系统在不正常输入或不正常外部条件下仍能够表现正常的程度。
要处理未期望的行为和错误终止,要准确、无歧义的向用户全面展示错误信息,这些错误信息有助于debug。
要考虑用户的各种行为、恶意攻击、极端情况,尽量封闭细节实现。
正确性:就是程序在正确输入下是正确的、严格按照spec的要求来的,是最重要的质量指标
对比:
正确性倾向于报错、健壮性倾向于容错。
对外倾向于健壮,对内倾向于正确。
5.Java的异常处理相关支持类
①Throwable(java.lang)
它是Exception和Error的父类,只有Throwable对象可以被虚拟机、throw语句抛出。
它包含线程的执行堆栈快照,以及一个用于提示更多错误消息的字符串
构造器可以接受两个参数,第一个参数是错误详细信息,第二个参数是异常发生的原因
setStackTrace()、getStackTrace()、printStackTrace()可以存储、获取、打印堆栈快照信息
initCause()、getCause()可以存储、获取异常发生的原因(也是一个Throwable对象)
getMessage、getLocalMessage可以获取错误详细信息,其中后者可以被重新,获得一些本地化处理后的错误详细信息。
②Error
Throwable的子类,表示不应该被捕获的严重错误,一般表示一些反常的情况,程序应该尽量取避免他们,而不是捕获、处理。
③Exception
Throwable的子类,暗示程序可能会需要在程序中去捕获他们,其中RuntimeException不是受查异常,其他都是受查异常。
④RuntimeException
通常是由程序员在程序中的不当处理导致的,可以避免。比如数组越界、null、除零错误、参数错误。
6.受查异常、非受查异常
受查异常:除了Error和RuntimeException的全部异常,必须要在程序中处理,要么用try_catch语句块进行捕获,要么抛出给上一级处理。可能需要进行一些恢复。
非受查异常:Error和RuntimeException异常,不需要进行捕获,应该尽量避免。知识简单打印异常信息就可以,不需要进一步处理。
7.finally
这个语句跟在catch(){}的后面,无论是否发生异常,都会执行。
finally执行的必要条件:虚拟机没有被关闭,线程没有被打断、try中的所有控制转移之前的语句全部执行完毕。
它会被插入到“控制转移语句”之前,也就是说,如果在try中返回0、在finally中返回1,最终程序返回的会是1
8.断言
主要用于开发阶段查错,实际上比较消耗性能,实际运行时要去掉或者禁用
不要用它检查来自于外部的错误,比如参数传的不正确等等。
如果断言所指定的条件不成立,那么立即产生一个AssertionError,程序终止
这个断言缺省条件下是关闭的,要加一个-ea参数
用法:
assert condition
assert condition message(用于弹出更多有用信息)
可以用来检查不变量、前置条件、后置条件等等。
9.测试
测试时发现问题,调试是解决问题。
白盒测试是对程序内部结构的测试,黑盒测试是对程序外部表现出来的行为的测试。
测试用例:不重不漏、简单有效。
要将输入的情况划分为若干等价类,每一个等价类里选出一个代表的测试用例即可。
比如要求n必须是正奇数
①是否是整数
②是否是正数
③是否是奇数
要考虑边界情况、边界两侧的情况、非常大或非常小的数据、上下限。
通过输入划分一些维度,然后每个维度考虑上面讨论的取值情况,然后将这些维度的情况进行组合,要保证每个维度的每个情况都要覆盖到。
测试策略要写在每个测试函数的上方,说明覆盖的情况。
ppt标准例子:
10.测试覆盖
路径覆盖:包含所有可能的控制流程,难度最大。
分支覆盖:覆盖所有分支语句的所有情况,难度中等。
语句覆盖:覆盖所有程序语句,难度最低。
八
1.内存管理模型
①以对象的方式管理内存,每个对象占据内存中连续的一段,分配在堆中。对象引用可以指向堆中的其他对象。非基本数据类型的对象等价于数据引用。
②基于栈和堆的内存管理都是动态分配,即在运行时动态分配内存,建立新的内存对象。而静态内存分配在编译阶段就已经确定好了内存分配。
③每个线程都有其线程栈,各自管理,彼此之间不可见。多线程之间传递数据,通过复制而非引用。栈无法支持复杂的数据类型。所有的局部的基本数据类型都在栈上创建。
④本地方法栈、PC
⑤Method Area(hotspot中称为perm,是堆的一部分,后改名metaspace)储存类信息、常量、静态变量。
总结各个变量、引用的存储位置:
栈:方法中基本类型变量及其值,方法中引用类型变量的引用
堆:方法中引用类型变量的实体对象,类的非静态成员变量(包括基本类型变量,引用类型变量)
方法区(也可以说是堆中):已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
2.GC中的基本概念
①根对象:判断可达性的起点,主要包括寄存器、静态区域数据、栈中数据所指的对象
②可达对象(活对象):从root可达的对象
③不可达对象(死对象):从root不可达的对象
④幽灵时间:对象从不可达到被回收的时间
3.四种GC策略
①引用计数:
为每个对象储存一个计数器,当有其他引用指向他的时候计数器+1,当其他引用与其断开连接时计数器-1,当计数器为0时回收。
优点:幽灵时间短
缺点:不全面容易漏、对并发支持弱、占用额外内存空间较大。
②标记-清除:
分为标记、清除两个阶段。标记阶段就是标注对象的活性(每个对象有一个标记位)。清除阶段就是清除被标记为死亡的对象。
优点:便于理解、操作时间短、与mutator松耦合、不移动对象。
缺点:需要停止程序以进行mark和sweep,导致幽灵时间过长,影响了程序本身的性能;造成了内存碎片化。
③标记-整理
和标记清除基本相似,唯一的区别在于回收的时候,把垃圾全都放到前面,把有用的放到后面,然后把前面的收走。
优点:避免碎片化
缺点:时间开销大,影响程序性能。
④复制
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。
优点:不容易产生内存碎片、分配内存开销比较小
缺点:空间开销大、随着对象数量的增加复制的代价也不断增大。
4.内存分区
分为新生代、老生代、永久代。
①新生代
又分为伊甸园、两个存活区
为了减少gc代价,使用copy进行垃圾回收。
当java分配对象堆内存的时候,首先在伊甸园进行分配。
当伊甸园满的时候,启动Minor GC,将伊甸园中的被引用的对象转移到S0生存区
伊甸园再次满的时候,第二次启动Minor GC,伊甸园和S0中的被引用对象转移到S1生存区。
第三次启动的时候,伊甸园和S1中的被引用对象转移到S0生存区。
如此进行下去,对于一个对象来说,他每在垃圾回收中存活一次,它的“代”就增加一次。
如果一个对象的代的数量超过某个临界值,下一次Minor GC中它将被转移到老生代。
②老生代
这一部分一般采用标记-整理、标记-清理等算法。
由新生代中代数比较大的对象复制而来。
如果老生代满了,就会触发Full GC
如果这一部分满了,则无法触发Minor GC
③永久代
如果这一部分满了,即无法分配更多储存元数据的空间,也启动Full GC
5.JVM性能调优
主要着手于垃圾处理部分,控制GC时长、GC频率
指定大小时,一般用<n>[g|m|k]这种形式,分别对应nGB,nMB,nKB这三种大小
System.gc()可以手动请求垃圾回收。
常用命令行参数:
-verbose:gc 输出每次gc的信息
-Xms<size> 初始堆大小
-Xmx<size> 最大堆大小
-XX:NewSize=<size> 初始年轻代大小(老生代初始大小为xms-newsize)
-XX:MaxNewSize=<size> 最大年轻代大小(老生代最大大小为xmx-maxnewsize)
-XX:NewRatio=<n> 新生代与老生代的比例是1:n,即新生代占堆的总内存的1/(n+1)
-XX:SurvivorRatio=<n> 伊甸园与存活区的比例是n:1,即每个存活区占新生代的1/(2+n)
-XX:MaxHeapFreeRatio=<n> 在一个代中,如果空闲内存的比例超过n%,这个代就会压缩,把空闲内存比例维持在n%以下
-XX:MinHeapFreeRatio=<n>在一个代中,如果空闲内存的比例低于n%,这个代就会扩张,把空闲内存的比例维持在n%以上
-XX:+PrintGC 打印GC信息
-XX:+PrintGCDetails 打印GC详细信息
-Xloggc:filename 将GC信息打印到日志文件中去
-XX:+UseSerialGC 对所有gc使用串行处理
-XX:+UseParallelGC -XX:ParallelGCThreads=n 对新生代使用并行GC算法,并制定线程数
XX:+UseParallelOldGC 对新生代、老生代都使用并行GC算法。
-XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=n 对老生代使用多线程的标记-清理算法,并指定线程数量。
–XX:+UseG1GC 对于较大容量的堆使用的一种新的垃圾回收算法。
九
1.线程与进程
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内部调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
线程生命周期:
一个程序至少一个进程,一个进程至少一个线程。
线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。
区别:
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
- 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 线程是处理器调度的基本单位,但是进程不是。
- 两者均可并发执行。
2.Java中线程创建与启动的几种方式
①继承Thread类,重写run()方法
public class MyThread extends Thread{//继承Thread类 public void run(){ //重写run方法 } } public class Main { public static void main(String[] args){ new MyThread().start();//创建并启动线程 } }
②实现Runnable接口,将它传递给Thread实例,然后执行
public class MyThread2 implements Runnable {//实现Runnable接口 public void run(){ //重写run方法 } } public class Main { public static void main(String[] args){ //创建并启动线程 MyThread2 myThread=new MyThread2(); Thread thread=new Thread(myThread); thread().start(); //或者new Thread(new MyThread2()).start(); } }
③实现Callable接口,创建实例,并传给FutureTask类,启动后用get()获得返回值。
public class Main { public static void main(String[] args){ MyThread3 th=new MyThread3(); //使用Lambda表达式创建Callable对象 //使用FutureTask类来包装Callable对象 FutureTask<Integer> future=new FutureTask<Integer>( (Callable<Integer>)()->{ return 5; } ); new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程 try{ System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回 }catch(Exception e){ ex.printStackTrace(); } } }
④使用Executor框架
Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。
终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
优势:
Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestCachedThreadPool{ public static void main(String[] args){ ExecutorService executorService = Executors.newCachedThreadPool(); // ExecutorService executorService = Executors.newFixedThreadPool(5); // ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 5; i++){ executorService.execute(new TestRunnable()); System.out.println("************* a" + i + " *************"); } executorService.shutdown(); } } class TestRunnable implements Runnable{ public void run(){ System.out.println(Thread.currentThread().getName() + "线程被调用了。"); } }
3.时间切片、竞态条件
多线程的执行是一个异步执行的过程,将进程执行过程划分为不同的片段交替执行,如果不加控制这个交替顺序是随机的
原子操作:不可被打断的操作,单行、单条语句未必是原子的。
竞态条件:两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。(消息传递机制也无法解决这个问题)
Thread.sleep(time) :使线程休眠,等待一段时间,但它不会让出监视器和锁的控制权。可以被打断。
Thread.yield() :建议调度器调度其他进程,可能没有任何作用,应该尽量避免使用
Thread.join() :让某个线程保持执行,直到其结束。可以被打断。
Thread.interrupt() :将某个线程的状态设置为“中断”,正常运行状态下没有任何效果。当执行到其他可被打断的时候,抛出一个受查异常InterruptedException。
Thread.isInterrupt():检测某个线程状态是否为“中断”。
4.线程安全策略
线程安全:ADT或方法在多线程中要执行正确
①限制数据共享 :线程之间不共享mutable类型的数据
②共享不可变数据:所有共享的资源都是immutable的。
③共享线程安全的可变数据:
StringBuffer是线程安全类,StringBuilder不是。
集合类都不是线程安全的,但是有一个让它变成线程安全的装饰器类。但他只能保证单个操作是线程安全的。
private static Map<Integer,Boolean> cache =Collections.synchronizedMap(new HashMap<>());
原子类也可以保证单个操作线程安全,他们在java.util.concurrent.atomic包中
AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型。
AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长整型数组里的元素。
AtomicReferenceArray:原子更新引用类型数组里的元素。
④使用同步机制
用锁、监视器、同步这一套体系确保线程安全
5.锁
逻辑上锁是对象内存堆中头部的一部分数据。JVM中的每个对象都有一个锁(或互斥锁),任何程序都可以使用它来协调对对象的多线程访问。如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(在锁内存区域设置一些标志)。所有其他的线程试图访问该对象的变量必须等到拥有该对象的锁有的线程释放锁(改变标记)。
一旦线程拥有一个锁,它可以多次请求相同的锁,但是在其他线程能够使用这个对象之前必须释放相同数量的锁。如果一个线程请求一个对象的锁三次,如果别的线程想拥有该对象的锁,那么之前线程需要 “释放”三次锁。
Java中显示锁的使用语法如下:
… private Lock bankLock = new ReentrantLock(); … public double getTotalBalance() { bankLock.lock(); try { double sum = 0; for (double a : accounts) sum += a; return sum; } finally { bankLock.unlock(); } }
1) 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
2) 锁可以管理试图进入被保护代码的线程
3) 锁可以拥有一个或者多个相关的条件对象
4) 每个条件对象管理那些已经进入被保护的代码段,但还不能运行的线程
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然后大多数情况下,并不需要这样的控制,并且可以使用一种嵌入Java语言的内部机制。从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就说,要调用该方法,线程必须获得内部的对象锁。
内部锁的一般用法如下:
public synchronized void transfer(int from, int to, double amount) throws InterruptedException { while (accounts[from] < amount) wait(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n", getTotalBalance()); notifyAll(); }
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁
6.同步
通过synchronized关键字来实现,他确立了一个先后关系,只有获得同步锁才能执行被同步锁住的代码。
7.线程安全策略的注释
首先,应该说明你用的是四种线程安全策略的哪一种。
如果是后两种,应该说明你的所有操作是原子的,与调度顺序无关
ppt上的例子:
8.死锁、活锁、饥饿
https://www.cnblogs.com/sunnyCx/p/8108366.html
1.死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
比如:迎面开来的汽车A和汽车B过马路,汽车A得到了半条路的资源(满足死锁发生条件1:资源访问是排他性的,我占了路你就不能上来,除非你爬我头上去),汽车B占了汽车A的另外半条路的资源,A想过去必须请求另一半被B占用的道路(死锁发生条件2:必须整条车身的空间才能开过去,我已经占了一半,尼玛另一半的路被B占用了),B若想过去也必须等待A让路,A是辆兰博基尼,B是开奇瑞QQ的屌丝,A素质比较低开窗对B狂骂:快给老子让开,B很生气,你妈逼的,老子就不让(死锁发生条件3:在未使用完资源前,不能被其他线程剥夺),于是两者相互僵持一个都走不了(死锁发生条件4:环路等待条件),而且导致整条道上的后续车辆也走不了。(很粗鲁的相互竞争)
2.活锁:线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去.(很绅士,互相谦让)
3.饥饿::这是个独木桥(单进程),桥上只能走一个人,B来到时A在桥上,B等待;
而此时比B年龄小的C来了,B让C现行(A走完后系统把进程分给了C),
C上桥后,D又来了,B又让D现行(C走完后系统把进程分个了D)
以此类推B一直是等待状态.
4.产生死锁的必要条件
(1)互斥使用(资源独占)
一个资源每次只能给一个进程使用(比如写操作)
(2)占有且等待
进程在申请新的资源的同时,保持对原有资源的占有
(3)不可抢占
资源申请者不能强行从资源占有者手中夺取资源,资源只能由占有者自愿释放
(4)循环等待
P1等待P2占有的资源,P2等待P3的资源,...Pn等待P1的资源,形成一个进程等待回路
5.活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
6.饥饿,是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。
饥饿是由资源分配策略决定的, 饥饿可以通过先来先服务等资源分配策略来避免。
实验中遇到的难点与解决途径
遇到的难点 | 解决途径 |
对于任务不太清晰,同时对于RI等概念不熟悉 | 对于任务问题多读了几遍要求,并着手完成就逐渐了解了;对于概念问题上课之后有了更深的了解。 |
对于泛型不是很理解 | 查找资料之后,比对c++的模板学习,有了更深的了解 |
部分时候github无法连接 | 使用加速器或等待网络情况正常均可解决 |
不知道如何读取txt文件内容 | 参考网上代码,使用bufferedread类进行文件流操作,先读入一行,之后利用split将String转为数字 |
正则表达式书写不正确 | 查询有关资料后多次尝试解决 |
读取文件时格式有问题 | 读取文件时去掉所有的换行符再进行处理 |
刚开始设计的时候没有头绪 | 在设计好基本的继承关系之后开始写代码,在写完一部分之后对于设计有了初步认识 |