1、当参与/的时候,两个数为整数的时候为整数除法,否则为浮点除法。
例:15/2=7,15.0/2=7.5。
2、整数被 0 除将会产生一个异常, 而浮点数被 0 除将会得到无穷大或 NaN 结果。
3、《算法+数据结构=程序》,这书名表示了程序员的工作方式。 即:先考虑如何操作数据,再考虑如何组织数据。而OOP则是调换了次序,先考虑组织数据,再考虑如何处理数据。
4、可以把类当作糖饼切割机,对象作为糖饼。由类构建对象的过程叫做创建类的实例。
5、封装: 封装的关键是绝不能让类中的方法直接访问其他类的实例域(属性)。程序仅通过对象的方法和对象的数据进行交互。
6、使用OOP的时候应该考虑的三个方面
对象的特征 | 应有的操作 |
---|---|
对象的行为 | 对象应该可以有哪些操作 (方法) |
对象的状态 | 对象拥有状态的时候进行操作会有什么不同 |
对象的标识(唯一性) | 如何辨别具有相同操作和状态的不同对象 |
(一)、类与对象
1、类之间的关系
- 依赖(“user-a”):一个类的方法操作一个类的对象
- 聚合(“has-a”):一个类中包含另一个类
- 继承(“is-a”):一个类继承另一个类
2、对于Date date = new Date(),实例化对象来说:一个变量对象没有包含一个对象,而仅仅引用一个对象。
3、隐式参数与显式参数:
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise; }
(double byPercent)就是显示参数,调用该方法的用户传递过来。
this.salary就是隐式参数:声明没有出现在方法体中。
4、方法参数--------值调用、引用调用。
5、仅当类没有提供任何构造器的时候, 系统才会提供一个默认的构造器
6、代码块:只要构造类的对象,代码块就会执行。首先运行代码块,再执行构造体的部分。
7、调用构造器的顺序:
- 所有实例域都设为默认值,int为0,boolean为false
- 根据类声明中的次序,依次执行所有实例域的初始化语句和代码块。
- 如果构造函数一调用了构造函数二,则进入构造函数二
- 执行构造函数体
8、包:
假如两个程序员不约而同地建立了 Employee类。只要将这些类放置在不同的包中, 就不会产生冲突。
9、类的设计技巧
- 保证类的数据私有性。
绝对不要破坏封装性。 - 一定要对数据初始化。
不要依赖系统的数据默认值,而是应该显示的初始化。 - 不要在类中使用过多的基本类型
用其他类代替相关的基本类型的使用 - 不是所有属性都需要访问器和更改器
一些属性初始化完了就只读或者不能读不能修改,例如:创建时间。 - 类名和方法名能展现出他们的职责
(二)、继承
1、重写
方法的名字和参数列表称为方法的签名。
- f(int)和f(String)是两个具有相同名字,不同签名的方法。如果在子类中定义了一个与超类相同签名的方法,则子类的这个方法就覆盖了超类的这个相同签名的方法。
- 重写方法的时候子类方法不能低于超类的可见性。
2、equals():
- 自反性:对于任何非空引用 x, x.equals(0)应该返回 true
- 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true , x.equals(y) 也应该返回
true。 - 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返 N true, y.equals(z) 返回
true, x.equals(z)也应该返回 true。 - 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。
- 对于任意非空引用 x, x.equals(null) 应该返回 false,
3、hashcode()
- 如果x和y是两个不同的对象,hashcode基本上不会相同
- 重写equals()之后,必须重写hashcode()
- 如果x.equals(y)为true,那么x.hashCode()就必须等于y.hashCode()。例如:定义的euals()比较用户的ID,那么hashCode()就需要散列ID,而不是雇员的姓名或者是存储地址。
4、(Object… argus)和(Object [] argus)都是接受任意数量参数。
5、反射
1、获取Class对象的三种方式:
- Object.getClass()
- Class.forName(“java.util.Random”)
- Random.class
6、继承的设计技巧
- 将公共属性放在超类
- 不要直接使用受保护的实例域
- 使用继承实现"is-a"的关系
- 除非所有的继承方法都有意义,否则不要使用继承
- 重写方法的时候,不要改变预期的行为
- 使用多态,而非类型信息
无论什么时候,对于下面这种形式的代码
if (x is oftype1)
action1(x);
else if (x is oftype 2)
action2(x)
都应该考虑使用多态性。
action1 与 3如action2 表示的是相同的概念吗? 如果是相同的概念,就应该为这个概念定义一
个方法, 并将其放置在两个类的超类或接口中,然后, 就可以调用
x.action(),以便使用多态性提供的动态分派机制执行相应的动作。
使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展
(三)、接口、lambda表达式、内部类
(一)、接口
1、接口不是类,而是对类的一组需求描述,这些类要遵循接口的需求描述的统一格式进行定义。
2、接口不能使用new关键词实例化一个接口,但可以声明接口的变量,如:List list = 。
3、虽然在接口中不能包含实例域或静态方法,但却可以包含常量。
在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。
4、与接口中的方法都自动地被设置为 public—样,接口中的域将被自动设为 public static final。
5、目前为止,静态方法一般放在接口的伴随类中。在标准库中, 你会看到成对出现的接口和实用工具类, 如 Collection/Collections 或 Path/Paths。
(二)、克隆
1、clone是object的一个protected方法,说明不能直接调用这个方法。只有Employee类可以克隆Employee类。
- 浅拷贝
(1)、这个限制是有原因的。想想看Object怎么来实现clone。它对这个对象一无所知,所以只能逐个域的拷贝,当对象的数据与都是数值或者其他基本类型,拷贝这些域没有任何问题。如果对象包含子对象的引用,拷贝就会得到相同子对象的引用,这样一来,原对象和克隆对象仍然会共享一些信息。
(2)、浅拷贝会有什么影响吗? 这要看具体情况。如果原对象和浅克隆对象共享的子对象是不
可变的, 那么这种共享就是安全的。如果子对象属于一个不可变的类, 如 String, 就是这 种情况。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。
(3)、不过, 通常子对象都是可变的, 必须重新定义 clone 方法来建立一个深拷贝, 同时克隆所有子对象。
2、深拷贝
对于每一个类,需要确定:
- 默认的 clone 方法是否满足要求;
- 是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法;
- 是否不该使用 clone()
实际上第 3 个选项是默认选项。如果选择第 1 项或第 2 项,类必须:
- 实现 Cloneable 接口;
- 重新定义 clone 方法,并指定 public 访问修饰符。
即使 clone 的默认(浅拷贝)实现能够满足要求, 还是需要实现 Cloneable 接口, 将 clone重新定义为 public,再调用 super.clone()。
(三)、lambda表达式(被赋给一个变量的函数)
在java8中,所有的lambda类型都是一个接口,而lambda表达式本身是接口的实现。
只有一个函数需要被实现的接口,我们叫做"函数式接口"。
(1)、Java 中的一种 lambda 表达式形式:参数, 箭头(->) 以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在 { }中,并包含显式的 return语句。例如:
(String first, String second) -> {
if (first.length() < second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0; }
(2)、即使 lambda 表达式没有参数, 仍然要提供空括号,就像无参数方法一样:
()-> { for (int i = 100;i >= 0;i-- ) System.out.println(i); }
(3)、如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型。例如:
Comparator<String> comp
= (first, second) // Same as (String first, String second)
-> first.length() - second.length();
在这里, 编译器可以推导出 first 和 second 必然是字符串,因为这个 lambda 表达式将赋给一个字符串比较器。
(4)、如果方法只有一 参数, 而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener = event ->
System.out.println("The time is " + new Date()");
// Instead of (event) -> . . . or (ActionEvent event) -> . . .
(5)、无需指定lambda表达式的返回类型,lambda表达式的返回类型可根据上下文推导而出。
(String first , String second) -> first.length() - second.length();
(6)、如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值,
这是不合法的。例如:(int x)-> { if(x >= 0) return 1; }
就不合法。
1、方法引用
System.out::println
是一个方法引用,它等价于x->System.out.println(x)
(四)、内部类
定义在另一个类中的类,就叫做内部类。
1、为什么要使用内部类:
- 内部类可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对其他包中的类隐藏起来。
- 当想要定义一个回调函数又不想编写大量代码的时候,就可以使用匿名内部类。
2、使用内部类访问对象状态
public class TalkingClock{
private int interval:
private boolean beep;
public TalkingClock(int interval, boolean beep) { . . . }
public void start(){ . . . }
public class TimePrinter implements ActionListener{
public void actionPe「formed(ActionEvent event) {
System.out.println("At the tone, the time is " + new OateO;)
if (beep) Toolkit.getDefaultToolki10.beepO;
}
}
}
外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类
引用的参数。因为 TimePrinter 类没有定义构造器,所以编译器为这个类生成了一个默认的构
造器,其代码如下所示:
public TimePrinter(TalkingGock clock){ // automatically generated code
outer = clock;
}
(五)、代理(proxy)
利用代理可以在运行时创建一个实现了一组给定接口的类。
(四)、异常、断言、日志
(一)、异常
- ERROR(错误):Error以及它子类的实例,表示的都是JVM的错误,错误不能被程序员的代码处理,error很少出现。
- EXCEPTION(异常):Exception以及他的子类,代表着程序运行时发送的各种不期望发生的事件。可以由程序员通过异常处理机制进行处理。
- 派生于Error类和RuntimeException类的所有异常称非受查异常,其他异常称为受查异常。
java异常层次结构图
(二)、需要抛出异常的情况
- 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。
- 程序运行过程中发现错误, 并且利用throw语句抛出一个受查异常(下一节将详细地介绍 throw 语句)。
- 程序出现错误, 例如,a[-l]=0 会抛出一个ArraylndexOutOffloundsException 这样的非受查异常。
- Java 虚拟机和运行时库出现的内部错误。
注意:
- 如果一个子类重写了超类中的方法,那么子类方法中的受查异常不能比超类中的受查异常更通用。
- 如果超类没有抛出异常,那么子类也不能抛出异常。
- 如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常, 或者这个类的任意一个子类的异常。 例如,FilelnputStream 构造器声明将有可能抛出一个 IOExcetion 异常, 然而并不知道具体是哪种 IOException 异常。它既可能是 IOException异常,也可能是其子类的异常,例如,FileNotFoundException0
(三)、再次抛出异常与异常链
- catch语句可以抛出异常
try{
access the database
}
catch (SQLException e) {
throw new ServletException("database error: " + e.getMessage());
}
- 有一种更好的解决方法:把原始异常设为新异常的"原因"
try{
access the database
}catch(SQLException e){
Throwable se = new ServletException("database error");
se.initCause(e);
}
当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCauseO ;
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异
常的细节。
(四)、finally语句
InputStream in = new FileInputStream(. . .);
try{
//1
code that might throw exception
//2
}catch(Exception e){
//3
show error message
//4
}finally{
//5
in.close()
}
//6
在上面这段代码中,有下列 3 种情况会执行 finally 子句:
1 ) 代码没有抛出异常。 在这种情况下, 程序首先执行 try 语句块中的全部代码,然后执行 finally 子句中的代码t 随后, 继续执行 try 语句块之后的第一条语句。也就是说,执行标注的 1、 2、 5、 6 处。
2 ) 抛出一个在 catch 子句中捕获的异常。在上面的示例中就是 IOException 异常。在这种情况下,程序将执行 try语句块中的所有代码,直到发生异常为止。此时,将跳过 try语句块中的剩余代码,转去执行与该异常匹配的 catch 子句中的代码, 最后执行 finally 子句中的代码。如果 catch 子句没有抛出异常,程序将执行 try 语句块之后的第一条语句。在这里,执行标注 1、 3、 4、5、 6 处的语句。如果 catch 子句抛出了一个异常, 异常将被抛回这个方法的调用者。在这里, 执行标注1、 3、 5 处的语句。
3 ) 代码抛出了一个异常, 但这个异常不是由 catch 子句捕获的。在这种情况下,程序将
执行 try 语句块中的所有语句,直到有异常被抛出为止。此时, 将跳过 try 语句块中的剩余代
码, 然后执行 finally 子句中的语句, 并将异常抛给这个方法的调用者。在这里, 执行标注 1、 5 处的语句
注意:„ 假设利用 return语句从 try语句块中退出。在方法返回前, finally 子句的内容将被执行。如果 finally 子句中 也有一个 return 语句,这个返回值将会覆盖原始的返回值
(五)、带资源的try语句
这里要读取一个文件中的所有单词:
try (Scanner in = new Scanner(new FileInputStream(7usr/share/dict/words")), "UTF-8") {
while (in.hasNextO)
System.out.pri ntl n(i n.next());
}
这个块正常退出时, 或者存在一个异常时, 都会调用 in.close() 方法, 就好像使用了finally块一样。
还可以指定多个资源: 例如:
try (Scanner in = new Scanne (new FileInputStream('7usr/share/dict/words"). "UTF-8");
PrintWriter out = new Pri ntWriter("out.txt")) {
while (in.hasNextO)
out.pri ntl n(i n.next().toUpperCaseO);
}
不论这个块如何退出, in 和 out 都会关闭。如果你用常规方式手动编程,就需要两个嵌
套的 try/finally语句。
带资源的 try 语句自身也可以有 catch 子句和一个 finally 子句。 这些子句会在
关闭资源之后执行。 不过在实际中, 一个 try 语句中加入这么多内容可能不是一个好
主意。
(二)、断言
如需计算 :
double y = Math.sqrt(x);
我们确信,这里的 X 是一个非负数值。原因是:X 是另外一个计算的结果,而这个结果
不可能是负值;或者 X 是一个方法的参数,而这个方法要求它的调用者只能提供一个正整数。
然而,还是希望进行检查, 以避免让“ 不是一个数” 的数值参与计算操作。当然,也可以抛
出一个异常:
if (x < 0) throw new 111egalArgumentException("x < 0");
但是这段代码会一直保留在程序中, 即使测试完毕也不会自动地删除。如果在程序中含
有大量的这种检查,程序运行起来会相当慢。
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测语句将会被自动地移走。
(1)、断言有两种形式:
assert 条件;
assert 条件 : 表达式;
这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。
在第二种形式中,表达式将被传人 AssertionError 的构造器, 并转换成一个消息字符串。
(三)、日志
(1)、通常, 有以下 7 个日志记录器级别:
• SEVERE
• WARNING
• INFO
• CONFIG
• FINE
• FINER
• FINEST
(五)、泛型
(一)、在 Java 中增加范型类之前, 泛型程序设计是用继承实现的。ArrayList 类只维护一个Object 引用的数组
public class ArrayList{ // before generic classes
private Object[] elementData;
...
public Object get(int i) { . . . }
public void add(Object o) { . . . }
}
这种方法有两个问题:
- 获取一个值的时候必须强制类型转换
ArrayList files = new ArrayList();
String filename = (String) files.get(0);
- 没有错误检查,可以向数组列表添加任意类型数据
files.add(new File("..."));
(二)、泛型提供了一个解决方法:类型参数
(三)、泛型代码和虚拟机
- 虚拟机没有泛型类型对象—所有对象都属于普通类。
(一)、类型擦除
- 无论何时提供一个泛型类型,都自动提供了一个对应的与原始类型。原始类型的名称就是删除类型参数后的泛型参数名。
例如, Pair 的原始类型如下所示:
public class Pair
{
private Object first;
private Object second;
public Pair(Object first, Object second) {
this,first = first;
this.second = second;
public Object getFirstO { return first; }
public Object getSecondO { return second; }
public void setFirst(Object newValue) { first = newValue; }
public void setSecond(Object newValue) { second = newValue; }
}
因为 T 是一个无限定的变量, 所以直接用 Object 替换。
结果是一个普通的类, 就好像泛型引人 Java 语言之前已经实现的那样。
在程序中可以包含不N类型的 Pair, 例 如, Pair 或 Pair。 而擦除类型后就变成原始的 Pair 类型了。
(六)、集合
(一)、队列
队列一般有两种实现方式:循环数组和链表
1、当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此, 只
有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。
Queue<Customer> expresslane = new CircularArrayQueue<>(100):
expressLane.add(new Customer("Harry"));
利用这种方式,一旦改变了想法, 可以轻松地使用另外一种不同的实现。只需要对程序
的一个地方做出修改, 即调用构造器的地方。如果觉得 LinkedListQueue 是个更好的选择,
就将代码修改为:
Queue<Custoaer> expressLane = new LinkedListQueue<>() ;
expressLane.add(new CustonierC'Harry"));
2、在研究 API 文档时,会发现另外一组名字以 Abstract 开头的类, 例如, AbstractQueue。
这些类是为类库实现者而设计的。如果想要实现自己的队列类(也许不太可能,) 会发现扩展
AbstractQueue 类要比实现 Queue 接口中的所有方法轻松得多。
3、在 Java SE 8中,甚至不用写循环。可以调用 forEachRemaining 方法并提供一 lambda表达式(它会处理一个元素)。 将对迭代器的每一个元素调用这个 lambda 表达式,直到再没有元素为止。
iterator.forEachRemaining(item ->{
System.out.println(item);
});
java迭代器查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用 next, 而在执行查找操作的同时, 迭代器的位置随之向前移动。
因此,应该将 Java 迭代器认为是位于两个元素之间。 当调用 next 时,迭代器就越过下
一个元素,并返回刚刚越过的那个元素的引用
4、iterator删除元素
如果需要删除元素,则需要越过当前元素
Iterator <String>it = c.iterator();
it.next();
it.remove();
更重要的是,对 next 方法和 remove 方法的调用具有互相依赖性。如果调用 remove 之前
没有调用 next 将是不合法的。如果这样做, 将会抛出一个 IllegalStateException 异常。
如果想删除两个相邻的元素, 不能直接地这样调用:
it.remove();
it.remove(); // Error!
相反地,必须先调用 next 越过将要删除的元素。
it,remove();
it.next();
it.remove(); // OK
5、
如果迭代器发现它的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个
ConcurrentModificationException 异常。例如, 看一看下面这段代码:
List<String> list = . .
ListIterator<String> iterl = list.listlterator();
ListIterator<String> iter2 = list.listlterator();
iterl.next();
iterl.remove();
iter2.next(); // throws ConcurrentModificationException
由于 iter2 检测出这个链表被从外部修改了, 所以对 iter2.next 的调用抛出了一个ConcurrentModificationException 异常。
为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。
(二)、集合中的接口
(三)、散列表
1、散列表为每一个对象计算一个整数,即:散列码。
2、如果自定义类,就要负责实现这个类的hashcode()方法,自己实现的hashcode方法和equals方法应该对应,即:a.equals(b)为true,那么a和b的hashcode也应该相同。
3、在 Java 中,散列表用链表数组实现。每个列表被称为桶( bucket) 要想査找表中对象的位置, 就要先计算它的散列码, 然后与桶的总数取余, 所得到的结果就是保存这个元素的桶的索引。例如, 如果某个对象的散列码为 76268, 并且有 128 个桶, 对象应该保存在第 108 号桶中(76268除以 128余 108 )。或许会很幸运, 在这个桶中没有其他元素,此时将元素直接插人到桶中就可以了。
4、当然,有时候会遇到桶被占满的情况, 这也是不可避免的。这种现象被称为散列冲突( hash
collision)
5、
- 如果想更多地控制散列表的运行性能, 就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性, 降低运行性能。
- 如果大致知道最终会有多少个元素要插人到散列表中, 就可以设置桶数。通常, 将桶数设置为预计元素个数的 75% ~ 150%。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是 2 的幂, 默认值为 16 (为表大小提供的任何值都将被自动地转换为 2 的下一个幂)。
- 当然,并不是总能够知道需要存储多少个元素的, 也有可能最初的估计过低。如果散列表太满, 就需要再散列(rehashed)。如果要对散列表再散列, 就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,. 然后丢弃原来的表。装填因子(load factor) 决定何时对散列表进行再散列。例如, 如果装填因子为 0.75 (默认值,) 而表中超过75%的位置已经填人元素, 这个表就会用双倍的桶数自动地进行再散列。对于大多数应用程序来说, 装填因子为0.75 是比较合理的。
- 散列表可以用于实现几个重要的数据结构。 其中最简单的是 set 类型。set 是没有重复元素的元素集合。set 的 add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。
- Java 集合类库提供了一个 HashSet类,它实现了基于散列表的集。可以用 add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中査找元素,而不必查看集合中的所有元素。散列集迭代器将依次访问所有的桶。 由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用 HashSet。