JAVA编程思想——分析阅读
准备:
前言
适用范围:Java SE5/6 版本。
Java的设计目标是:为程序员减少复杂性,缩短代码的开发时间,跨平台复用。
学习方法:一模式或一节点就进入一练习,思维与实践并行,现学现卖。
每当我认为我已经理解了并发编程时,又会有新的奇山峻岭等待这我去征服。——作者都花了好几个月写并发这一篇章并发出这样的感慨,我们又有什么理由妄自菲薄呢。
绪论
学习语言时:需要在头脑中创建一个模型,以加强对这种语言的深入理解;如果遇到了疑问,就将它反馈到头脑的模型中并推断出答案。
一、对象导论
1 知识
人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。类型即指所抽象的是什么,也可以说用的是什么类型的语言。Java,C ,汇编,Python等。其中想C或者汇编要基于计算机的结构来求解问题,面向过程;而Java 等面向对象的语言是基于问题来求解,面向对象。
面向对象的5个基本特性:
- 万物皆对象。抽象
- 程序是对象的集合,它们通过发送消息来告知彼此所要做的。对象之间的方法调用
- 每个对象都有自己的由其他对象所构成的存储。封装
- 每个对象都拥有其类型。class,继承
- 某个特定类型的所有对象都可以接受同样的消息。多态
类实际上就是一个数据类型,程序员根据需求,通过添加新的数据类型(class)来扩展编程语言,而不需要像面向过程语言那样只能使用现有的用来表示机器中的存储单元的数据类型。
类创造出来的对象就是服务提供者(好处:有助于提高对象的内聚性,或者说通过设计模式的六大原则来设计对象提供服务),通过将问题的解决分解成对象集合的方式去调用(现有类库)和设计创建对象。
访问控制的原因:
- 让调用者无法触及他们不应该触及的部分,且通过控制符可以让调用者很容易地区分哪些东西对他们很重要(public),哪些是可以忽略的(private)。
- 允许类或库设计者可以改变类内部的工作方式而不用担心会影响到调用方。
子类通过添加新方法(is like a 关系 )和覆盖父类方法(is a 关系)来改变与父类的差异。(但要满足里式替换原则)
单(跟)继承的好处:
- 确保所有对象都属于同一个基本类型。
- 保证所有对象都具备某些功能。
- 极大简化参数的传递。(如参数可以直接放Object对象类型?)
- 使垃圾回收器的实现变得容易得多。
使用不同的容器选择点是:
- 不同容器提供了不同类型的接口和外部行为。
- 不同的容器对于某些操作具有不同的效率。(例如:ArrayList查找快,增删慢(相对于LinkedList);LinkedList查找慢,增删快)
最后,最好要对其他语言(如python)有个清晰的认识,java语言是否符合项目的设计及未来的发展需要。
2 疑问
什么是CGI?
CGI(Common Gateway Interface)公共网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。其实就是我们通常说的接口。
3 思想总结
面向对象语言JAVA的特性是:抽象、封装、继承、多态。使用面向对象语言可以让我们更好的面向问题来设计解决程序,也更容易读懂和维护该程序的代码,相对于面向过程语言来说。
二、一切皆对象
1 知识
Java通过操作引用来操纵对象,就像我们用遥控器来操纵电视机一样。
5个不同的地方存储数据
- 寄存器。最快,因为它在处理器内部。
- 堆栈。速度仅次于寄存器,位于RAM,必须知道确切的生命周期来移动堆栈指针分配和释放内存。基本数据类型和对象引用存储在堆栈中。
- 堆。通用内存池也位于RAM。堆不同于堆栈的好处是不需要知道存储数据或对象的生命周期,因此分配和释放内存需要更多时间。用于存放所有的Java对象。
- 常量存储。存放在程序代码中或者只读的ROM(只读存储器),因为不变所有是安全的。
- 非RAM存储。一般是流对象和持久化对象。在程序不运行时也可以存在。
基本数据类型如下:
基本类型的值存储在堆栈中,每种类型所占存储空间大小不变,因此更具可移植性。
BigInteger(支持任意精度的整数) 和 BigDecimal (支持任意精度的定点数,货币计算)属于高精度数据类型,以方法调用代替运算符实现运算,因此运行速度较慢,是以速度换取精度的数据类型。
PS: 定点和浮点的区别
类的成员变量与局部变量是否需要初始化的区别:
类的成员变量如果是基本数据类型,即使没有初始化值,java也会确保它获得一个对应类型的默认值,防止程序错误。而java不会对局部变量进行默认初始化,如果局部变量没有初始化赋值,则会编译报错说变量没有初始化。
类的成员变量默认初始化值如下:
PS:构建程序时通过反写域名作为包名来避免名字冲突。(com.xixi)
2 疑问
什么是处理器?什么是寄存器?处理器内部结构是什么样的?为什么寄存器存储最快?
这是计算机组成原理的知识,需要补补。
堆栈通过上下移动堆栈指针来分配和释放内存,那么多线程的时候是如何分配内存的呢?
栈是线程私有的,因此多线程就有各自的栈,互不干扰。
3 思想总结
万物皆对象!
三、操作符
1 知识
别名现象是因为java操作中操作的是对对象的引用,所以会出现别名现象。注意方法参数传递是基本数据类型是值传递,对象类型是”引用传递“(如果替换整个对象没事,但如果修改对象内的属性值的话,原有对象会发生变化)。
整数乘除会直接去掉结果的小数位,不是四舍五入。
a++ :先生成值,再执行++运算。
++a : 先执行++运算,再生成值。
类对象比较时 : == 或者 != 比较的是对象的引用(即地址),而equals()比较的是内容。
普通的对象用equals()比较的还是引用,和 == 一样,要想比较内容就得重写equals()方法。
基本数据类型比较直接用 == 或者 !=
java指数的写法
//编译器通常会将指数作为双精度(double)处理
double e = 1E-43d;// = 10 -43次方
double f = 1.6E43d;// = 1.6 * 10 43次
2 疑问
Random(47)的使用?随机数怎么有效地创造及生成随机数的注意事项。
A:Random(47)使用见下:
@Test
public void randomTest(){
//如果不传参数生成随机数对象Random,则java会把当前时间作为随机数生成器的种子seed,则每一次执行
//都会产生不同的输出。而如果传入固定值的seed,则每一次输出都是可预见性的相同的值,每一次nextInt //的返回值按次序都是相同的,固定seed方便测试验证。
Random r = new Random(47);
//每次调用nextInt方法传入的参数如100表示所产生的随机数的上限,下限为0,如果不想得到0的结果可以 +1
System.out.println( r.nextInt(100)+1);//随机数小于上限且不等于
System.out.println( r.nextInt(100)+1);
System.out.println( r.nextInt(100)+1);
//Random r1 = new Random();
}
如何重写equals()方法比较内容?
单精度和双精度的区别?
8进制和16进制的写法?
@Test
public void otherTest(){
int c = 0x2f;//16进制 零x
System.out.println(Integer.toBinaryString(c));
int d = 0177;
System.out.println(Integer.toBinaryString(d));
//int e = 01987; //前面如果加了零,表示这个数是8进制的数,每位最大值不能超过7
}
java 按位操作符 & | ^ ~ 和移位操作符<< >> >>>的使用?在算法中用的是否普遍?
3 思想总结
一些基本的操作符使用。
四、控制执行流程
1 知识
无穷循环的形式
for(;;)
//或者
while(true){
//里面没有结束条件break;
}
禁止使用标签跳跃,防止滥用,程序混乱。
switch 新特性:与 enum 枚举或 String 一起使用。
吸血鬼数字解法
参考答案
@Test
public void xixueguiTest(){
int num =0;
for (int i=10;i<100;i++){
for (int j=i+1;j<100;j++){
int target=i*j;
if (target<1000||target>9999){
continue;
}
num++;
int[] targetNum = { target / 1000, target / 100 % 10, target / 10 % 100 % 10, target%10 };
int[] strNum = { i % 10, i / 10, j % 10, j / 10 };
Arrays.sort(targetNum);
Arrays.sort(strNum);
if (Arrays.equals(targetNum,strNum)){
System.out.println(target + " = " + i + " * " + j);
}
}
}
System.out.println(num);
}
2 疑问
3 思想总结
一些普通的流程控制,如 while , for , break , continue , switch
五、初始化与清理
1 知识
初始化
创建对象时通过自动调用构造器来确保初始化。
类和方法的命名:名字起的好可以使系统易于理解和修改。
方法重载或构造器重载相当于人类语言的冗余性——可以从具体的语句中推断出含义。重载的规则由参数类型,个数,顺序的不同来确定,一般不推荐顺序不同。注意,返回值的不同不能用于重载,因为有时候调用有返回值的方法并不必须要返回值,这样编译器无法区分是调用哪个。
基本数据类型的重载思想是能从一个较小类型如int自动提升至一个较大类型如double,如果要把较大类型如double转为较小类型如long则必须强转。这部分我觉得除非不得已,绝对不进行这种自动提升重载方法,不便于理解。
this关键字的使用场合:
- 只有当需要明确指出对当前对象的引用时,才需要使用this关键字。如需要返回对当前对象的引用时,
- return this.
- 将当前对象传递给其他方法时(作为参数)。
- 一个类中有多个构造器,构造器之间调用另一构造器使用this。this(a,b),this(a)。注意构造器调用必须置于第一行,因此构造器调用其他构造器一次只能调用一个,要调用多个就要构造器间嵌套调用。注意:只能构造器调用构造器,构造器禁止被其他方法调用。
- 通过构造器给类成员变量赋值,如 this.a = a;
静态初始化只有在必要时刻(类第一次加载.class文件时:一般是类对象的第一次创建或第一次直接用类访问静态数据时)才会进行。之后无论创建多少对象,静态数据都只占用一份存储区域。
PS:构造器实际上也是static静态方法。
对象的创建过程:
- java解释器查找类路径,定位如Monkey.class 文件。
- 载入Monkey.class,执行所以静态初始化动作。
- 在堆上为Monkey对象分配足够的存储空间。
- 存储空间清零,Monkey对象的所以类成员变量置为默认值,如0,false,null。
- 执行所有类成员变量的初始化动作。
- 执行构造器。
用代码块来初始化类成员变量的实例与静态成员变量的初始化差不多,代码块来初始化类成员变量的实例也在构造器之前执行,区别在于静态成员变量的初始化只有一次,而代码块来初始化非静态类成员变量在每次创建对象时都会执行。
数组初始化
编译器不允许指定数组的大小,数组的创建是在运行时刻进行的。数组的长度一旦确定则不可变。创建数组为非基本数据类型时,该数组为引用数组,数组元素存储的是引用对象的引用(地址)。
注意:通过花括号{}初始化列表时最后一个逗号是可有可无的。
@Test
public void arrayTest(){
//数组内元素的值会根据数据类型自动初始化为空值,如int为 0.
int[] a = new int[5];
Integer[] b = {1,2,new Integer(3)};
//注意构建格式 new String[]{};
String[] maomao = new String[]{"mao","mao","hong"};
}
focus: 数组使用参考
清理
垃圾回收注意事项:
- 对象可能不被垃圾回收。
- 垃圾回收并不等于”析构“(c++)。
- 垃圾回收只与内存有关。
fanalize()方法用于释放为本地方法(一种在java中调用非java代码的方式)分配的内存。
GC前,会调用finalize()方法,所以可以重写finalize()方法来验证终结条件。
System.gc():强制GC(不一定触发)
自适应垃圾回收技术思想依据:对于任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。
类的初始化顺序:
成员变量定义的先后顺序决定了变量初始化的顺序。而类成员变量初始化是最早的,然后是构造器,再然后是方法。
可变参数列表
应用场合:参数个数和类型未知时使用。
可变参数列表本质上还是数组,当我们指定参数时,编译器实际上会为什么去填充数组,所以我们可以用foreach迭代遍历。可变参数列表也可以接受其对应类型的数组当做可变参数列表。
可变参数列表不依赖于自动包装机制,实际使用的是基本数据类型,且可变参数列表可以使用自动包装机制装箱拆箱。
推荐:在使用可变参数列表时,如果有重载方法,则应该只使用一个版本类型(如下)的重载方法,而不是参数类型个数不同等重载方法。
private void printArray(int a,Object... obj){
for (Object o : obj) {
System.out.print(o);
}
System.out.println();
}
private void printArray(float a,Object... obj){
for (Object o : obj) {
System.out.print(o);
}
System.out.println();
}
枚举enum
常量命名:大写字母及下划线。
enum与switch是绝佳的组合,我们可以将enum用作另外一种创建数据类型的方式。
2 疑问
在创建一个类时,在定义时就被初始化了的String域域通过构造器初始化的String域两种方式的差异?
A: 差别在于strA一开始被初始化为"strA",而strB先被初始化为null,然后再被初始化为"strB" .
class Test{
private String strA = "strA";
private String strB;
Test(){
strB = "strB";
}
}
代码块来初始化非静态类成员变量的实例如何支撑匿名内部类的初始化?
java枚举的使用?有什么特别实用的使用技巧么?
3 思想总结
初步理解java的初始化和自动清理过程,注意数组的初始化使用,可变参数列表适用于参数个数和类型未知的场合,枚举enum用于自定义数据类型,并最好能与switch配合使用最佳。
六、访问权限控制
1 知识
访问权限控制的前景交代是:迭代,重构,需求变更等为了增加可读性、理解性和可维护性。
访问权限控制修饰词:把变动的事物与保持不变的事物区分开,是对具体实现的隐藏,即封装。(package和import)
一个类只有一个public class类,其他类都不算public 的,它们主要是为主public类提供支持的。
用导入限定的命名空间位置描述来提供一个管理名字空间的机制。
java包命名规则是全部使用小写字母连接。
有冲突名字情况下,则必须返回到指定全面的方式。
将构造器私有(private),则不能通过构造器来创建对象,而必须调用专门的如getInstance()方法来创建对象。且因为构造器是私有的,他阻止对此类的继承。
protected 修饰符处理的是继承的概念(包含包访问权限)。
类只能定义包访问权限和public权限,如果是默认的包访问权限,则该类不能被非同一个包下的类创建访问。
java比c的名字空间控制好在:
- package 定义每个类的包路径
- 包命名模式(域名反写)
- 关键字import
2 疑问
如何把类库空间置于包中?或者说设计项目时如何分配类的位置和创建包分类。
这个一般有个默认的分类。可以参考这个分类。
java如何实现条件编译?如何定义程序为调试版和发布版?现在这样定义还有价值么>
private在多线程环境下的重要性?
被private修饰的私有方法不会有线程安全问题。
3 思想总结
访问权限控制的原因有二:
- 隔离不属于客户端所需接口的部分实现封装,也使客户端能更清楚地理解哪些对他们是重要的和哪些是可以忽略的。
- 让方法或接口提供者可以安全地更改和迭代类内部的工作方式和结构。
权限是约定俗成的东西,权限的修饰符可以更好地方便于程序的理解迭代和维护。
七、复用类
1 知识
复用类的方式有二:组合与继承。
初始化类成员变量的方式有四:
- 在定义对象的地方。这样则总能在被构造器调用之前被初始化。
- 在类构造器中。
- 在使用这些对象之前,称为惰性初始化(一般是使用前判空,为空则初始化该对象)。
- 使用实例初始化。(代码块等)
继承自动得到父类中所有的域和方法,所有一般在设计继承时会将所有的成员变量(数据成员)指定为private,所有的方法指定为public。
子类调用父类方法用super关键字(如:super.action() )。
当创建一个子类对象时,该对象包含了一个父类的子对象。因此当子类创建对象时,会先去创建父类子对象,及父类对象,然后再初始化子类的成员变量,最后调用子类的构造器。(p130练习5)PS:创建父类时会调用父类构造器【注意:每次创建子类时都会去调用父类构造器创建一个父类的子对象,即每次创建子对象都会调用父类构造器创建对象,不是想static常量那样只创建一次的】,如果是默认构造器则可以不写,默认调用父类的无参构造器,如果是只有带参数的构造器,则必须在子类构造器第一行显式调用父类构造器【super(3) 】。
组合与继承
组合和继承都允许在新的类中放置子对象(成员变量),组合是显示地这么做,而继承是隐式的这么做。一般组合中的成员变量会修饰为private,为了安全而隐藏具体实现。
组合与继承的选择在于两种之间的关系是"is -a"还是 "has-a"关系。选择继承时要判断自己需不需要从新类向父类进行向上转型,需要才可以用继承。
向上转型和向下转型用于有继承结构的对象,方便一视同仁地操作对象。
final关键字
final 修饰一般表示这是无法改变的。
不可改变的理由:设计或效率。
final数据
一个既是static又是final的域只占据一段不能改变的存储空间。
对于基本类型,final使数值恒定不变,而对于对象引用(数组也是),final使引用恒定不变,但对象自身却可以被修改。因此,使引用成为final没有使基本类型成为final的用处大。
空白final:指被声明为final但又未给定初值的域。可以通过构造器初始化final域来对一个类的final域初始化值,使之可以根据对象的不同而有所不同,且又保持恒定不变的特性。
final参数:无法在方法中更改参数引用锁指向的对象或基本参数的值(PS:参数引用的对象的值还是可以更改,太鸡肋,没用)。可以读参数,无法修改参数。主要用来向匿名内部类传递数据。
final方法
使用的原因:一是锁定方法,防止被继承覆盖重写修改该方法的含义。二是效率。(在虚拟机是HotSpot已经没必要了)
类中所有private方法都隐式的指定为是final的。但因为方法是private的,所有只在该类中私有使用,不影响其他继承类也继续叫这个方法名。
final类
被final修饰的类不可被继承。final类的域是可变的,和其他的一样,除非被final修饰;而final类的所有方法都无法覆盖重写,因为final禁止继承,所以的方法都隐式指定为final的。final类的方法就无需加final修饰了。
设计类时,除非明确不想被覆盖,否则不应给类或方法加final修饰,既没有效率提高可能,又可能会妨碍其他人通过继承来复用这个类。
继承与初始化(p146案例)
- 一开始会先加载父类的static静态变量初始化,
- 然后再加载子类的static静态变量初始化,
- 再然后是初始化父类的成员变量为默认值,
- 然后是初始化子类的成员变量为默认值,
- 然后是调用父类的构造器,
- 再然后调用子类的构造器。
//典型常量定义方式:
/**
*public 公开的 ; static 静态的,只有一份 ; final 常量,不可变的
*/
public static final int VALUE_TWO = 3;
//final修饰的数据只能在运行时才能确认他的值,编译时是不能确认他的值的,从可以把随机值赋值给final域得知
static final int INT_A = rand.nextInt(20);
其他:
toString()方法会在编译器打印一个String而传递的是一个对象时自动调用该对象的toString()方法,因此如果想要使要打印日志的对象具备这样的行为时只有编写一个toString()方法,否则打印的是对象的地址类型组合信息。
每个类都可以创建一个main()方法,方便单元测试,且无需删除。即使一个程序中含有多个类,也只有命令行锁调用的哪个类的main()方法会被调用,及我们run的那个类方法。
ArrayList 代替了 Vector;
HashMap 代替了 HashTable
2 疑问
final参数主要用来向匿名内部类传递数据,对于引用参数的内部的值是可以改变的,加final有什么意义么
所以要看场合添加,不是为了防止内重写或者确定是不可变的对象特别是基本类型值时,没有必要添加。
3 思想总结
多用组合,少用继承。
设计类时要遵循单一职责,继承是要遵循里式替换原则。
final关键字主要用在设计上想得到不可变元素、方法和类上时。
设计系统时应考虑程序开发是增量过程,如图人类的学习;要有扩展性,像进化的生命体而不是想设计摩天大楼一样快速见效?
八、多态
1 知识
多态的作用:消除类型之间的耦合关系。
绑定:将一个方法调用同一个方法主体关联起来被称作绑定。
前期绑定:在程序执行前进行绑定,如面向过程语言C。
后期绑定:在运行时根据对象的类型进行绑定,如JAVA。
Java除了static、final(private也属于final)方法之外,都是后期绑定。所以当我们声明一个方法为final时,意思是关闭动态绑定。
多态的"缺陷"
- 父类中只有非private 方法才能被覆盖。否则其实在子类中名字相同的只是个全新的方法,并没有覆盖。
- 成员变量在多态时访问的是父类的成员变量值(如果成员变量相同的时候,这时候子类包含这两个名字相同的成员变量值,一个是父类的,通过super.field调用,一个是子类的,this.field),方法访问的是子类的复写方法(如果有覆盖重写的话)
- static静态方法是类方法,与对象无关,不具有多态性
在实际工作中,我们一般不会这样,一般会把成员变量都修饰为private(再通过get/set方法提供访问),且对于父类和子类的成员变量命名也不会相同,避免引起混淆。
//多态
Shape s = new Cycle();
//普通创建对象
Cycle c = new Cycle();
多态和构造器
构造器的特殊任务:检查对象是否被正确地构造。
复杂对象调用构造器的顺序:
- 在对象创建之前,将分配给对象的存储空间初始化为二进制零。
- 调用父类构造器,这个步骤会递归传递到Objcet对象。
- 按照声明顺序调用成员变量的初始化方法。
- 调用子类构造器的主体。
注意:每一个类的初始化都遵循如果第一次调用的话,会有static成员变量先初始化,然后是类的成员变量的初始化,再然后是构造器的调用。这一调用顺序在父类或者成员对象的调用中都适用。当然要注意的是如果类的static成员变量已经不是第一次初始化则不会再调用了。
PS:如果父类在初始化的构造器中调用覆盖的方法,则根据多态调用的其实是子类覆盖的方法,只是由于子类还未初始化,其中如果有成员变量的话则值为0.
编写构造器准则:
用尽可能简单的方法使对象进入正常状态;如果可以的话,构造器避免调用其他方法。构造器唯一可以安全调用的是final修饰的方法(包括private)。其他的会多态到子类上去。
协变返回类型允许我们写方法时返回更具体的对象类型。比如不是Shape而是Cycle.
通过继承表达行为间的差异,并用成员变量(相同的接口,不同的类型赋予不同的子类)表达状态上的变化。
注意:
多态时创建的对象调用只能是父类有的方法,因为多态时引用是向上转型的,子类的扩展方法会“丢失”,如果要使用子类的扩展方法则要向下转型。如果向下转型不成功(不是该类型或其父类)则会报ClassCastException(类转型异常)。
2 疑问
不同的类型修饰构造器有什么区别,一般private是不想让对象被创建,用于单例,那public、protected、和默认的使用有什么讲究么?
这个注意就是看作用的范围域了,在使用时尽可能地缩小范围,而不是一开始就使用public修饰,除非是对外提供的方法。
static可以修饰构造器么?
A:构造器就是默认的static了,所以不允许。confirmed.
不同访问权限的修饰符修饰的static成员变量、类成员变量、构造器初始化的先后顺序是按照声明顺序来的么?
如果是private static 修饰的静态类,只会在明确调用到它时才会初始化,参考单例的静态内部类写法,利用了静态内部类延迟初始化的特性。
3 思想总结
多态让程序针对一体性类型的对象统一处理有了可能,让程序的开发更加迅速,代码编写更加人性化处理,也使得扩展和更加容易。
但是也要注意多态的缺陷,那就是多态针对的是对象的公共行为,对象的静态方法和对象成员变量及私有行为(private、final)都是不能多态的。
还有,因为多态增加了对象的组织复杂和庞大,所以使用的原则是多用组合,少用继承。
九、接口
1 知识
接口和内部类(特别是匿名内部类)为我们提供了一种将接口与实现分离的更加结构化的方法。
抽象类:
它是普通的类与接口之间的一种中庸之道。特别是在不能使用纯接口的时候。抽象类适用于重构,这样我们可以将公共方法沿着继承层次结构向上移动。(PS:只有类名上修饰了abstract则不管有没有抽象方法,该类都是一个抽象类,不能被创造出对象。当然更可以全部是abstract,这种其实就是接口了。)
接口:
接口可以包含成员变量。其成员变量都是static和final的(使接口成为便捷的用来创建常量组的工具,不过如果是enum枚举类型常量的话最好还是用enum,直观好看),即静态常量;接口内所有的方法和成员变量都是public的,无论是否写public修饰。
如果接口定义时不加public修饰符(接口或者类里面的接口定义),则该接口只有包访问权限,只能在同一个包内使用。引申出接口可以嵌套在类或其他接口中。
Java通过多接口实现多重继承,其他具体实现类或者抽象类都只能单继承。
在接口和抽象类选择中尽量选择接口来设计。(作者的建议是前期如果没有必要可以直接选择设计类而不是接口,看中需要性)
在继承和实现的接口中方法名一样时,一起按照重写和重载的规则来,相同则只要有一个实现就行(或者重写),不同的则看方法签名不同实现重载。(如果只是返回值类型不同则无法重载编译器会报错无法实现)
接口配合策略模式和适配器模式使得程序更加地灵活。
2 疑问
接口中的类呢?也是常量么?
接口中可以放置嵌套类(内部类),并且是自动public
和static
的。
3 思想总结
面向接口编程才能解耦,依赖倒置。
十、内部类
1 知识
一个类的定义在另一个类的定义内部,就是内部类。
内部类的使用可以很方便地隐藏实现细节。
注意:注意是要在另一个类的内部,如果是在外面就是一个普通的类,当然该类不能是public的,因为一个类文件只能有一个public类型的类;如果是内部类的话,则可以是public的。
内部类可以直接访问其外部类的方法和成员变量,包括private等所有的方法。(这是因为内部类对象在创建时会秘密捕获一个指向外部类的引用。)
内部类还是一个完整的类,跟其他类一模一样,有类的访问权限限制等。区别在于
- 一:内部类依赖于外部类,因此。内部类对其外部类是完全透明可见的,包括其private的私有成员变量,外部类也能访问。
- 二:要注意内部的定义的作用域,超出作用域则内部类不可用。
在方法和作用域内的内部类
内部类可以定义在任何地方,包括方法的参数,方法内部(不能用private修饰),方法的作用域内(局部内部类)。
匿名类不可能有构造器。(想要用的话可以用父类的带参构造器,父类不能是接口才行,必须是普通类或者抽象类)
匿名内部类可以有字段,方法,还能够对其字段执行初始化操作。
匿名内部类使用外部定义的对象或值时,编译器要求其参数引用是final类型的。如果是通过匿名内部类的父类构造器传递参数进来的话,则不需要是final类型的,因为这个参数并不会被匿名内部类直接使用。所以加final修饰是为了保证直接使用时该内部类的成员变量不可变?
匿名内部类与正规的继承区别:
匿名内部类既可以扩展类,也可以实现接口,但不能两者兼备。如是是实现接口,也只能实现一个接口。
嵌套类
把内部类声明为static类型的我们称为嵌套类,嵌套类不需要内部类对象与其外部类对象之间有联系。
- 创建嵌套类对象,不需要外部类的对象。
- 不能从嵌套类的对象中访问非静态的外部类对象。(因为嵌套类没有保存外部类对象的引用,不需要依赖外部类)
嵌套类与内部类的区别
普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static域,也不能包含嵌套类。但是嵌套类可以有static数据和static域,也能包含嵌套类。
嵌套类是内部类的一种,其编译后的class文件还是在外部类文件中。
在接口中写的内部类因为是接口里面的,自动是public和static的,所以接口中的内部类是嵌套类。我们可以在接口中放置内部类(嵌套类)代码。可以用于创建某些公共代码,使得他们可以被某个接口的所以不同实现所共用。
内部类无论嵌套了多少层的内部类,它都能透明地访问所以它嵌入的外部类的所有成员(即使是外部类的private成员、方法)。
为什么需要内部类
内部类实现一个接口与外部类实现这个接口的区别:
外部类实现一个接口不是总能享受到接口带来的方便,有时需要用到接口的实现。(比如外部类已经有同名的方法实现了,无法重复覆盖写出想要的覆盖方法)
这时候由内部类实现接口的优势:每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
内部类使得多重继承的解决方案变得完整:内部类允许继承多个非接口类型。(类或抽象类,因为每个内部类都可以去继承不同的类,这样这个外部类就有了多种继承的能力)
实现2个接口时我们可以选择使用单一类(多实现),或者使用内部类来实现。而如果实现的是2个抽象类或者具体的类,而不是接口,就只能使用内部类才能实现多重继承。
内部类使用前提:如果不需要解决多重继承的问题,那么自然还是用别的编程方式。
使用内部类可以获得的特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
- 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。(实现不同的行为效果,有点策略模式的意思)
- 创建内部类对象的时刻并不依赖于外部类对象的创建。(通过static方法调用或者创建的是嵌套类?)
- 内部类并没有令人迷糊的"is -a"关系,他就是一个独立的实体。
主要用来响应事件的系统被称为事件驱动系统。
注意这个处理事件结合的写法,把要迭代处理的事件用一个新的集合包装,再处理完毕之后对原有的处理事件集合进行移除事件(在此期间可能会原事件集合还可能会进行添加待处理事件,所以这样处理逻辑是极好的),这样就不会影响到迭代循环的长度。
通过内部类,使变化的事务与不变的事务相互分离(模板方法)。内部类允许:
- 控制框架的完整实现是有的那个的类创建的,从而使得实现的细节被封装起来。内部类用来表示解决问题所必需的各种不同的action()。
- 内部类能够很容易地访问外部类的任意成员,所以可以避免这种实现变得笨拙。
内部类覆盖
如果两个外围类都有相同名字的内部类,而这两个外围类是继承关系的话,这时候这两个内部类是完全独立的两个实体,各自在自己的命名空间内,没有继承关系。而当这两个内部类一个明确继承另外一个的时候,则他们便有继承关系。和普通类继承没什么不同。
局部内部类
局部内部类指在代码块里面的内部类,典型如方法体的里面。局部内部类不能有访问说明符(public权限访问符),其他的和内部类一样。局部内部类与匿名内部类的使用区别:
- 我们需要不止一个该内部类的对象。
- 我们需要一个已命名的构造器,或者需要重载构造器。匿名内部类做不到,因为匿名内部类只能用于实例初始化。
如果不是这些需要,还是直接用匿名内部类好了。
其他知识点补充:
访问说明符:指的是访问权限符,如public,private。
回调:
参考
如下图所示, 回调是一种双向的调用方式, 其实而言, 回调也有同步和异步之分, 讲解中是同步回调, 第二个例子使用的是异步回调 。
回调的思想是:
- 类A的a()方法调用类B的b()方法
- 类B的b()方法执行完毕主动调用类A的callback()方法
通俗而言: 就是A类中调用B类中的某个方法C, 然后B类中反过来调用A类中的方法D, D这个方法就叫回调方法。
//创建内部类对象
Outer outer = new Outer;
Outer.Inner inner = outer.getInner();
//内部类的基本使用如下
public class Outter {
public class Inner{
public void getShow(){
//两种调用方法,第一种比较明显能展示该方法是外部类的方法
Outter.this.show();
show();
}
public Outter getOuter(){
//内部类中,this表示该内部类,Outter.this 表示内部类对应的外部类的引用对象
return Outter.this;
}
}
//静态内部类则不需要创建外部类对象,直接用类调用方式
public static class StaticInner{
public static void staticShow(){
System.out.println("staticShow");
}
}
public void show(){
System.out.println("outer");
}
public Inner getInner(){
return new Inner();
}
public static void main(String[] args) {
Outter outter =new Outter();
//内部类的创建可以如下:使用外部类的引用.new语法创建内部类对象
Outter.Inner inner = outter.new Inner();
//也可以通过外部类方法来创建一个
Inner in = outter.getInner();
in.getShow();
inner.getShow();
inner.getOuter().show();
Outter.StaticInner.staticShow();
}
}
//=====================================================
//有参匿名内部类使用方法 前提是得有普通类或者抽象类(带有参数构造器)实现。
public abstract class Fishing {
int i =0;
public Fishing() {
}
public Fishing(int i) {
this.i = i;
}
public void fish(){
System.out.println("fish"+i);
}
}
public class Human {
public Fishing getFishing(int a){
return new Fishing(a){
private String name = "maomao";
@Override
public void fish() {
super.fish();
}
};
};
}
public static void main(String[] args) {
Human human = new Human();
Fishing fishing = human.getFishing(10);
fishing.fish();
}
}
//=====================================================
//p199 用匿名内部类实现工厂方法
public class Platform {
public static void ride( CycleFacrory facrory){
facrory.getCycle().getName();
}
public static void main(String[] args) {
ride(Bicycle.facrory);
ride(Unicycle.facrory);
ride(Tricycle.facrory);
}
}
public class Bicycle implements Cycle {
//静态单例工厂创建
public static CycleFacrory facrory = new CycleFacrory() {
@Override
public Cycle getCycle() {
return new Bicycle();
}
};
@Override
public void getName() {
System.out.println("Bicycle");
}
}
//工厂接口
public interface CycleFacrory {
public Cycle getCycle();
}
//产品接口
public interface Cycle {
public void getName();
}
2 疑问
静态类里的方法没有加static修饰还是静态方法么?
A:不是,待详细解答。
为什么方法内部的内部类不能用private修饰?
匿名内部类使用外部定义的对象或值时,编译器要求其参数引用是final类型的,为什么呢?
静态类里的类(static修饰的class)和方法都没有static修饰,属于静态方法和静态常量么?
嵌套类的作用?
普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static域,也不能包含嵌套类。但是嵌套类可以有static数据和static域,也能包含嵌套类。为什么通的内部类不能有static数据和static域?
创建内部类对象的时刻并不依赖于外部类对象的创建。为什么不需要?什么时候不需要?(通过static方法调用或者创建的是嵌套类?)
闭包?闭包的作用?
回调?回头网上结合研究下
enclossingClassReference.super(); ?继承内部类时为什么要用这个?
P245 练习26里如何生成一个带参数的构造器?目前能实现的只能是带继承的内部类的外部类的引用参数,而不能创建内部类自带的参数,因为这样无法继承了。待处理?
3 思想总结
内部类的使用主要用于配合弥补接口无法达到的多重继承的时候使用,还有用于封装实现,达到解耦。
其实如非必要,在设计阶段尽量规避使用内部类实现,大部分情况下单继承及接口实现便能满足需求了。
十一、持有对象
1 知识
基本的容器使用:List、Set、Queue、Map。
数组使用的局限:数组具有固定的尺寸的局限性,使用起来不够灵活。
集合容器通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。
如果不需要使用每个元素的索引(对索引有操作),可以使用foreach来迭代数据。
使用多态来创建容器,但是,当想要使用具体容器类的额外的功能时,就要直接创建具体类。如下:
List list = new ArrayList();
//List没有peek()方法
LinkedList linkedList = new LinkedList();
linkedList.peek();
//Arrays.asList();底层表示还是数组,因此不能调整尺寸,不能使用add()或delete(),但可以修改set()
List list1 = Arrays.asList();
//通过显示类型参数说明,告诉编译器对于Arrays.<Snow>asList();产生的List类型,这样才能编译成功。
List<Snow> list2 = Arrays.<Snow>asList();
//数组容器的打印 必须使用方法
Arrays.toString()
//其他容器不需要,直接toString()即可(即直接传入对象就可以调用默认toString()打印容器)。
//Collection打印出来的内容用方括号括住,逗号分隔。[1,2,3,4]
//Map打印出来的内容用大括号括住,逗号分隔,键和值由等号联系。{rat=1,tiger=2,monkey=3,drogon=4}
ArrayList 和 LinkedList 都是List类型,它们都按照被插入的顺序保存元素。
HashSet、 TreeSet 、 LinkedHashSet 都是Set类型,Set的保存的值都是唯一的,存储元素方式的区别:
HashSet: 通过散列值来存储元素,使用HashSet可以最快地获取元素。
TreeSet: 按照比较结果升序来保存对象,如果存储顺序很重要用TreeSet 。
LinkedHashSet : 对存储顺序,按照被添加的顺序保存对象。
Map: 也称关联数组,像一个简单的数据库。
HashMap:提供最快的查询技术,不是按照明显的顺序保存元素。
TreeMap:按照比较结果升序来保存键。
LinkedHashMap:按照被添加的顺序保存对象,同时保留了HasMap的查询速度。
List
ArrayList :优势在随机访问元素,但是在List的中间插入和移除元素时较慢。
LinkedList :优势在与在List的中间插入和移除元素时代价较低,并提供了优化的顺序访问。缺点是在随机访问方面相对比较慢。
P256 ListFeatures.class 演示了ArrayList 的主要常用操作方法。
ArrayList的indexOf()、remove()方法针对的是对象的引用操作及对象(比较基于equals()方法),这样就不会因为相同的对象也能被remove。
ArrayList通过containsAll()方法来判断包含关系,并不会因为排序和顺序问题而不同,比较的只是元素的包含关系。
retainAll()是取交集的操作。
isEmpty()判断容器集合是否为空
clear()清除集合操作。
Pet[] pet = list.toArray(new Pet[0]);//返回一个具有合适尺寸的数组。
迭代器
//迭代器模式,不关心具体的容器集合类型
Iterator iterator = list.iterator();
//ListIterator只能用于各种List类的访问。可以进行双向移动,往前往后,且可以增删改查。
ListIterator listIterator = list.listIterator();
LinkedList
LinkedList 还添加了可以使其用作栈、队列或双端队列的方法。有些方法作用相同而名字不同,是为了在特定用法的上下文环境中更加适用(特别是在Queue)如getFirst()和element()与peek().三个都是获取第一个元素的方法,前面两个如果List为空则抛出异常,而第三个为空时返回null。
还有其他一些也是一样的。
TreeSet: 按照比较结果升序来保存对象,我们还可以对TreeSet传入我们想要的排序特性。
//不区分大小写字母排序
SortedSet set = new TreeSet(String.CASE_INSENSITIVE_ORDER);
Stack
LinkedList具有能够直接实现栈的所以功能的方法,因此可以直接将LinkedList作为栈使用。
类名之后的告诉编译器这将是一个参数化类型,当类被使用时会被实际类型所替换,就是T.
Java.util.Stack 设计欠佳。
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<>();
public void push(T v){
storage.addFirst(v);
}
public T peek(){
return storage.getFirst();
}
public T pop(){
return storage.removeFirst();
}
public boolean empty(){
return storage.isEmpty();
}
@Override
public String toString(){
return storage.toString();
}
}
Set
Set不保存重复的元素。
Set最常被使用的是测试归属性,经常要询问某个对象是否在某个Set中,因此查询是Set中最重要的操作。而,使用HashSet可以最快地获取元素。
//使用contains()方法测试归属性
boolean contains = set1.contains("a");
Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。
Map可以返回它的键Set(因为键都是唯一的),它的值Collection(因为它的值可以是相同的,所以不是set),或者它的键值对的Set(是个EntrySet)。
@Test
public void mapTest1() {
Map<Integer,Integer> map = new HashMap();
map.put(Integer.valueOf(1),1);
map.put(Integer.valueOf(2),1);
boolean containsKey = map.containsKey(1);
System.out.println("containsKey="+containsKey);
boolean containsValue = map.containsValue(1);
System.out.println("containsValue="+containsValue);
Set<Integer> keySet = map.keySet();
System.out.println("keySet="+keySet);
Collection<Integer> values = map.values();
System.out.println("values="+values);
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
System.out.println("entrySet="+entrySet);
}
Queue
队列是一个典型的先进先出(FIFO)容器。
队列在并发编程中特别重要,因为它可以安全地将对象从一个任务传输给另一个任务。
LinkedList 可以作为Queue的一种实现,因为它实现了Queue接口。
queue.offer()方法将一个元素插入到队尾。
peek()和element()在不移除的情况下返回队头。peek()在队列为空是返回null,element()则抛异常。
poll() 和 remove() 移除并返回队头。poll()在队列为空是返回null,remove()则抛异常。
PriorityQueue
优先级队列的下一个弹出元素是最需要的元素(具有最高的优先级)。
PriorityQueue 确保当我们调用peek()、poll() 和 remove()时,获取的元素是队列中优先级最高的元素。
如果需要,可以通过提供自己的Comparator来修改这个顺序。
@Test
public void queueTest() {
Queue<String> queue = new LinkedList();
String[] split = "i am iron man".split(" ");
for (int i = 0; i < split.length; i++) {
queue.offer(split[i]);
}
while (queue.peek() != null) {
//System.out.print(JSON.toJSONString(queue.remove()));
System.out.print(queue.remove()+" ");
}
}
@Test
public void queueTest() {
Queue<String> queue = new LinkedList();
String[] split = "i am iron man".split(" ");
for (int i = 0; i < split.length; i++) {
queue.offer(split[i]);
}
while (queue.peek() != null) {
//System.out.print(JSON.toJSONString(queue.remove()));
System.out.print(queue.remove()+" ");
}
java.util.List<Integer> list = Arrays.asList(19, 2, 3, 5, 7, 10);
PriorityQueue<Integer> priorityQueue = new PriorityQueue(list);
//注意,必须用方法打印出来,如果只是用JSON.toJSONString(priorityQueue)是不能得到效果的,因为存储并不固定,而是通过获取元素时比较来确定优先级的
while (priorityQueue.peek()!= null){
System.out.print(priorityQueue.remove()+" ");
}
System.out.println();
//System.out.println(JSON.toJSONString(priorityQueue));
PriorityQueue<Integer> priorityQueue1 = new PriorityQueue(list.size(),Collections.reverseOrder());
priorityQueue1.addAll(list);
while (priorityQueue1.peek()!= null){
System.out.print(priorityQueue1.remove()+" ");
}
// System.out.println(JSON.toJSONString(priorityQueue1));
}
我们可以通过参数化Collection来表示容器之间的共性或者实现iterator()方法来实现迭代器功能。C++没有Collection而是通过迭代器来表示容器的共性。Java都有。
foreach:
只要我们创建的类实现了Iterable接口,就能用foreach语法糖。Iterable接口包含一个能够产生Iterator的Iterator()方法。
foreach语句可以用于数组或其他实现了Iterable接口的类,但注意:数组不是一个Iterable。
其他知识:
@Suppress Warnings (unchecked):表示只有有关“不受检查的异常”的警告信息应该被抑制。这样就不会有黄色警告了。
默认的toString()方法将打印类名@散列码(例:Apple@11b86e7 。是hashCode()方法生成的无符号16进制数)。
容器结构图:
2 疑问
既然LinkedHashMap按照被添加的顺序保存对象,同时保留了HasMap的查询速度。为什么我们不用LinkedHashMap替换HashMap?其实还是有影响的?
LinkedHashMap效率肯定没有HashMap高,毕竟还要维护一个插入的先后顺序,如果不需要关心插入顺序,那用HashMap就好了,时间复杂度是O(1).
ArrayList 和 LinkedList 只是在List中插入数据快慢有区别?在默认add方法插入有性能区别么?LinkedList 缺点是在随机访问方面相对比较慢,那么迭代器顺序访问区别还很大么?
要回答这个问题需要了解ArrayList 和LinkedList 底层的数据结构。ArrayList 底层是数组实现的,因此它要随机访问一个值只要根据数组下标即可,而LinkedList底层是链表,则需要从头到尾遍历一遍去寻找才行。相对于的,链表结构增删特别是在任何位置增删都非常快,只要更换头尾节点的引用即可,而数组则在对中间位置进行插入时则会导致其他元素要进行位置腾挪。
ArrayList 这些后面尖括号里面的值是有区别的么?还是只是一个类型代称?
这只是泛型的一个描述,没有具体意思。
SortedSet set = new TreeSet(); SortedSet 不是 TreeSet的父类或者接口,为什么也能多态呢?
A: 是有接口继承关系的。可以在idea中右击类选择diagrams-show diagrams.查看类继承结构。
Set对于其填充的值只能add一种类型么?那为什么ArrayList可以是Objcet类型的?set不行?
public class TreeSet<E> extends AbstractSet<E>{
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
不是这么理解的,表示的是可以指定为一直类型的泛型。
Map.Entry<Integer, Integer> ?map.entrySet();返回的这个entry,entrySet是个什么类型的数据结构?本质是其实就是个Map么?
Map.Entry<K,V> 这个结构其实就是一个节点的意思,在java 8 中直接就用节点Node名字去继承实现Entry。
static class Node<K,V> implements Map.Entry<K,V> {}
3 思想总结
数组一旦生成,其容量或者说长度是不能改变的,这是数组使用的局限性。
Collection保存单一的元素,而Map保存相关联的键值对。
如果需要进行大量的随机访问,则使用ArrayList,如果经常从表中间插入或删除元素,则使用LinkedList。
Queue 或Stack(栈,不是java.util里面的那个),由LinkedList 提供支持。
新程序不应该使用过时的Vector、Hashtable、Stack。
总的其实只有四种容器:Map、List、Set、Queue
十二、通过异常处理错误
1、如何编写正确的异常处理程序
2、如何自定义异常。
1 知识
使用异常的好处(对编程语言来说加入异常的好处):
- 降低处理错误代码的复杂度,否则处理错误代码繁多且臃肿,影响正常核心逻辑的理解和处理。
- 只需要在一个地方处理错误(异常处理程序,catch块)。
- 将正常代码和问题处理代码相分离。
异常最重要的方面之一就是如果发生问题,它将不允许程序沿着其正常的路径继续走下去。C或 C++不行,尤其是C没有任何办法强制程序在出现问题时停止在某条路径上运行下去。
我们抛出异常时总是用new在堆上创建异常对象。异常一般会有默认构造器或者接受字符串作为参数的构造器,以便把相关信息放入异常对象的构造器;或者两种都有。
只有匹配的catch子句才能得到执行,执行后变跳出异常处理程序块,有finally则执行,否则结束。
异常处理的两种模型:
- 终止模型。将异常抛出不处理。
- 恢复模型。修正错误,重新尝试调用出问题的方法。
创建自定义异常
要自定义异常类,必须从已有的异常类继承.对异常来说,最重要的部分就是类名,异常的名称应该望文生义。如果需要自定义异常描述信息则添加有参构造器即可。
异常说明
即在方法后加上声明可能会抛出的异常,告知此方法的调用者。
public void exceptionTest() throws TestExecption {}
如果方法没有throws 异常说明,则说明此方法不会抛出任何异常(也有可能被try catch了)。
PS: 任何继承了RuntimeException的异常(包含它自己,这些称为不受检查异常,属于错误,将被自动捕获),是可以在没有异常说明的情况下被抛出的,因为这时候是运行时异常。
在定义设计抽象基类和接口时可以预留声明方法将抛出异常,方便子类和接口实现类可以抛出预先声明的异常。
我们可以在catch处理时通过捕获Exception基类来捕获所以类型的异常,Throwable 也可以。所以如果要分批catch处理,那么最好将Exception放在末尾,防止它抢先匹配捕获异常。
//会重新装填异常信息,如果调用fillInStackTrace,那么会把调用的那一行变成异常的新发生地。
//fillInStackTrace()方法在thow 异常时的Throwable构造器里面调用封装异常信息
fillInStackTrace();
异常链
异常链:在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来。
两种方式实现吧其他类型的异常链接起来:
- 继承Error、Exception、RuntimeException(主要是后面两个),实现带cause参数的构造器。
- 调用initCause()方法连接异常。
@Test
public void exceptionTest() throws Exception {
//throw new TestExecption();
try {
//throw new TestExecption("i am god");
//throw new TestExecption(new NullPointerException());
throw (Exception) new TestExecption().initCause(new IndexOutOfBoundsException());
} catch (Exception testExecption) {
//默认输出标准错误流
testExecption.printStackTrace();
//可以指定输出流——标准流
testExecption.printStackTrace(System.out);
}
}
Error用来表示编译时和系统错误,一般程序员不关心;Exception是可以被抛出的基本类型,是程序员应该关心的。
只能在代码中忽略RuntimeException及其子类的异常(因为RuntimeException是种错误,无法执行下去了),其他类型异常的处理都是由编译器强制实施的。
如果一个方法中有多个return,包括finally块也有,则最后返回的是finally块里的return.
使用try+finally则就是异常也正常执行,这种情况下可以丢失异常。
通过强制子类遵守父类方法的异常说明对象的可替换性就得到了保证。子类不能抛出大于父类异常声明的类型,最大的是Throwable。
父类抛出异常,这时子类方法可以不抛出任何异常。因为不影响已有的程序。
不要在构造器中打开资源什么的,否则构造器就算能执行finally或者catch关闭也不好。
通用的清理资源规则是:在创建需要关闭资源,清理对象之后,立即进入一个try-finally语句块。
异常匹配
抛出异常的时候,异常处理程序会按照代码书写顺序找出“最近”的处理程序。匹配处理之后将不再继续查找。catch会捕获其异常类及所有从它派生的异常。
当我们还不知道怎么handle 异常的时候把异常catch了,导致后面异常被吃了,这种是有问题的。
所有模型都是错误的,但有些是能用的。
异常处理的可选方式(重要)
被检查异常的优缺点:
优点是一次说明能增加开发人员的效率,并提高代码的质量,对小项目,小程序友好。
缺点是对于大项目来说,过多的异常类型声明及检查导致项目无法管理,开发效率下降,也不能很好的提高代码质量。
总的来说,Java的”被检测异常“带来的麻烦比好处要多。
原因是被检查异常强迫程序员在不知道该采取什么措施的时候提供异常处理程序,这是不现实的。(亚信的代码就是这样,方法里一堆的异常声明;优车就好很多,控制得很好。)
异常机制及强静态类型检查必要的原因是:
- 不在于编译器是否会强制程序员去处理错误,而是要有一致的、使用异常来报告错误的模型。
- 不在于什么时候进行检查,而是一定要有类型检查。必须强制程序使用正确的类型,置于这种强制是在编译器还是运行时并不重要。
减少编译时施加的约束能显著提高程序员的编程效率。反射和泛型就是用来补偿静态类型检查所带来的过多限制。
好的程序设计语言能帮助程序员写出好程序,但无论哪种语言都避免不了程序员用它写出坏程序。
对于被检查异常的处理方式:
- 把异常传递给控制台。就不需要写try-catch处理了。
- 把”被检查异常“ 转换为 ”不被检查异常“。方法有2:
- 即把”被检查异常“包装进RuntimeException,这样方法也不用异常声明(因为RuntimeException是不被检查异常,不需要声明或者处理)。
- 创建自己的RuntimeException子类,这样抛出的异常也是不受检查的,也不需要异常声明或者try-catch处理。
@Test
public void exceptionTest() {//不需要声明异常 throws Execption
try {
throw new TestExecption();
} catch (TestExecption e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
异常使用指南:
- 在恰当的基本处理问题。(即在知道该如何处理异常的情况下才捕获异常。)
- 解决问题并且重新调用产生异常的方法。(很少这样处理)
- 进行少许修补,然后绕过异常发生的地方继续执行。(一般是异常不影响流程或者可以忍受,直接忽略异常往下执行。)
- 用别的数据进行计算,以代替方法预计会返回的值。(也很少用到)
- 把当前运行环境下能做的事情进来做完,然后把相同的异常重新抛到更高层。(跟第三点差不多,这个也有用到)
- 把当前运行环境下能做的事情进来做完,然后把不同的异常重新抛到更高层。
- 终止程序。
- 进行简化。(如果异常模式使得问题变得太复杂,则相对恼人)
- 让类库和程序更安全。
2 疑问
什么叫析构函数?好像是指垃圾回收?
析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作。
为什么要用finally来关闭资源,关闭文件呢?不关闭会怎么样?
不关闭就造成文件描述符无法释放,属于一种系统文件的浪费
不关闭可能造成对文件的写入丢失,写入有可能存在缓存区,没有关闭并且没有主动flush到具体的文件上,则可能造成丢失。
如果该文件被文件锁独占,那么就会造成其他线程无法操作该文件。
Too many open files错误,操作系统针对一个进程分配的文件描述符表是有限大小的,因此打开不释放可能造成该表溢出。
try+finally 的使用场景?是对异常忽略的时候用?
- 对异常的捕获处理
- 加锁解锁操作
- 对文件或流的打开和关闭
构造器也可以抛异常?为什么要抛异常?相当于一个方法是么?
可以抛异常,构造器本质上也是一个方法,是用于对对象的构造的初始化。
3 思想总结
java异常处理机制将正常的逻辑与异常部分分开,让我们可以更专注地分别处理这两个问题。
异常处理的报告功能是异常的精髓所在,即错误日志打印或沉淀等。
一致的错误报告系统意味着我们再也不必对所写的每一段代码都质问自己是否有错误被遗漏。
十三、字符串
1 知识
String对象是不可变的。String对象方法等之间的传递实际上是引用的一个拷贝。而该引用所指向的对象一直待在单一的物理位置上,从未动过。
String对象具有只读特性,所以指向它的任何引用都不可能改变它的值,也不会对其他的引用有什么影响。
在使用字符串连接时如果拿不准用哪种方式,可以用javap来分析程序代码。查看java代码是如何工作的可以用javap反编译代码:
javap -c Demo //(类名) -c表示将生成JVM字节码
重载“+” 与StringBuilder
通过javap反编译可看到,在使用重载“+”时,编译器或自动帮我们创建一个StringBuilder对象来构造连接出最终的String对象。但是要注意的时,在循环体里如果还是用重载“+”来连接String对象的话,编译器自动创建的StringBuilder是在循环体内产生的,这意味着每一次循环都会创建一个新的StringBuilder对象。因此,在连接循环体内的String对象时,要自行创建StringBuilder对象在循环体外append()拼接,并且在使用append()方法连接时禁用append(a+ ":" +c)这种投机取巧的方式,否则编译器又会自动创建一个StringBuilder对象来连接内部字符串操作;如果是简单的字符串连接(特别是不是循环体的连接的话),可以信任编译器来处理。
StringBuilder是线程不安全的,但也因此效率比StringBuffer快一点。
@Test
public void stringTest() {
StringBuffer sb = new StringBuffer();
sb.append("a").append("+").append("b").append("=").append("c").append(" ok");
//delete(int start, int end) 可以用于删除不想要的字符串连接。
sb.delete(sb.length()-2,sb.length());
System.out.println(sb);
}
无意识递归
public class InfiniteRecursion {
@Override
public String toString() {
//String"+"会自动类型转换为Sting,会想把InfiniteRecursion转化成String,这时候调用toString方法,又会到了原点,造成无意识递归,所以禁用this
//return "InfiniteRecursion{}"+this;
//正确的做法是用super.toString()
return "InfiniteRecursion{}"+super.toString();
}
public static void main(String[] args) {
List<InfiniteRecursion> list = new ArrayList<>();
for (int i =0;i<5;i++){
list.add(new InfiniteRecursion());
}
System.out.println(list);
}
}
String的基本方法:P321
@Test
public void stringTest1() {
String 啊 = "我是一个大头兵";
char c = 啊.charAt(6);
System.out.println(c);
String a = "i am iron man";
char c1 = a.charAt(6);
System.out.println(c1);
//char是字符形式
char[] b = new char[50];
a.getChars(0,a.length(),b,0);
System.out.println(b);
//byte 是字节形式
byte[] bytes = a.getBytes();
System.out.println(Arrays.toString(bytes));
//生成字符串的所以字符数组
char[] chars = a.toCharArray();
System.out.println(chars);
boolean we = a.contentEquals("i am iron man");
boolean b1 = a.contentEquals(new StringBuffer("i am iron man"));
System.out.println(we +" "+ b1);
//比较string是否相等
boolean regionMatches = a.regionMatches(3, "am iron man", 0, "am iron man".length());
System.out.println(regionMatches);
System.out.format("test%d",5);
}
Java intern() 方法
Formatter 是个解释器
正则表达式
一般来说,正则表达式就是以某种方式来描述字符串。
java语言对反斜线\的处理与其它语言不同。
如: \d = java : \ \d .
\ \ \ \ :java用此表示一条普通的反斜线。
斜线是:/ : 左正右反 :\
正则表达式中有特殊意义的字符都要通过\ \ 双反斜线来转义。如+ :\ \ +
如果正则表达式不是只使用一次的话,非String对象的正则表达式具备更加的性能。如用于匹配手机号格式等的工具类。
匹配规则:
CharSequence
接口CharSequence是从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义。所以这些实现了CharSequence的类都可以用于接收CharSequence参数的方法的使用;多数正则表达式操作也都接收CharSequence类型的参数。
Pattern和Matcher
使用Pattern pattern = Pattern.compile("[1].*。$");编译正则表达式。
组(Groups)
组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组号为1表示被第一对括号括起来的组,以此类推。
//三个组 0:ABCD 1: BC 2:C
A(B(C))D
组的概念用得少,先不仔细了解。
例子:
@Test
public void matchTest() {
//通过String的方法如matches是应用正则表达式最简单的途径
boolean b = "-1234".matches("-?\\d+");
System.out.println(b);
//330
//在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。 定义为私有静态类常量
// private static Pattern NUMBER_PATTERN = Pattern.compile("[0-9]+");
// 说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则)
//首字母大写,句号结束。注意.*都是特殊有特殊含义的,不需要转义,转义了就变成普通字符了
Pattern pattern = Pattern.compile("^[A-Z].*。$");
boolean matches = pattern.matcher("W水电费个胜多负。").matches();
System.out.println(matches);
pattern = Pattern.compile("W");
//如果是用于replace、split方法则不要加(^$)
String s = pattern.matcher("SSSSW水电费个胜多负。").replaceAll("WWW");
System.out.println(s);
String[] split = pattern.split("SSSSW水电W费个wW胜多负W。");
System.out.println(Arrays.toString(split));
//限制分割字符串数量最多3个
String[] split1 = pattern.split("SSSSW水电W费个wW胜多负W。", 3);
System.out.println(Arrays.toString(split1));
pattern = Pattern.compile(",");
String target = "i want to say , you either die hero, or live long enough to see youself become a villain,";
StringBuffer sb = new StringBuffer();
Matcher matcher = pattern.matcher(target);
while (matcher.find()) {
//) 将当前匹配子串替换为指定字符串,并且将替换后的子串以及其之前到上次匹配子串之后的字符串段添加到一个StringBuffer对象里
//appendReplacement可以对匹配到的数据做不同类型的处理,通过方法提供不同的replacement即可。
matcher.appendReplacement(sb, returnRandom());
System.out.println(sb);
}
//将最后一次匹配工作后剩余的字符串添加到一个StringBuffer对象里。
System.out.println(matcher.appendTail(sb));
//将matcher对象重新设置到当前字符序列的起始位置
matcher.reset();
System.out.println( matcher.replaceAll(""));
//应用新的字符序列
matcher.reset("happy ending , ok?");
System.out.println( matcher.replaceAll(""));
}
private static Random random = new Random(47);
private String returnRandom() {
int i = random.nextInt(20);
return String.valueOf(i);
}
正则表达式可用于如日志搜索,如linux系统中的grep 命令。
如下,通过对每一行reset匹配对象,然后如果find为true,则通过group打印出值和start打印出位置。
Scanner
Scanner类大大减轻了扫描输入的工作负担。Scanner构造器可以接受任何类型的输入对象,包括File、String、InputStream、Readable等对象。所有的输入、分词以及翻译操作都隐藏在不同类型的next方法中。
例子:
/**
* 防火墙日志文件扫描
*/
private static String log =
"10.25.253.123@31/12/2018\n"+
"11.25.253.123@31/11/2018\n"+
"12.25.253.123@31/10/2018\n"+
"13.25.253.123@31/09/2018\n";
@Test
public void fireScannerTest(){
Scanner scanner = new Scanner(log);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})";
while (scanner.hasNext(pattern)){
//scanner.next(pattern)如果不需要可以不返回值,但一定要调用,如果不调用scanner.next(pattern),那么匹配指针位置就会一直不动保持在第一位,在while循环模式下一直循序下去不结束
String next = scanner.next(pattern);
//返回匹配值
System.out.println(next);
MatchResult match = scanner.match();
//match.group(0)指整个匹配值,1是第一个括号的值;2是第二个括号的
System.out.println("group0="+match.group(0));
System.out.println("ip="+match.group(1)+"===date="+match.group(2));
}
}
2 疑问
javap反编译出来的程序代码是汇编语言么?怎么看?
不是,是java自己的定义的八种原子操作,这个要参考操作文档查看具体的意思就懂了。
String.contentEquals(CharSequence cs)方法中的CharSequence 是什么类型?
A:接口CharSequence是从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义。所以这些实现了CharSequence的类都可以用于接收CharSequence参数的方法的使用;多数正则表达式操作也都接收CharSequence类型的参数。
收集正则表达式中的一些生活常用语句,如邮箱、手机号等匹配规则。
定界符?
A:就是 设定界限的 符号。 比如字符 a,就需要用单引号做定界符 'a'; 比如字符串 abc,就需要用双引号做定界符 "abc"。 就是 告诉计算机: 字符开始了a字符结束了。 字符串开始了abc字符串结束了。
在正则表达式中定界符就是(^.***$)
3 思想总结
总的来说一般字符串的操作就是一些普通常用的操作,只要熟练就行,如果需要字符串拼接的话看情况选用拼接方法。正则表达式的匹配一般是用在像手机号,电子邮箱这种的验证上,比较简单;而如果是用在日志搜索匹配解析的话就比较复杂,针对解析日志等文件需要Scanner来处理更方便灵活。
十四、类型信息
1 知识
RTTI——Run-Time Type Identification:在运行时识别一个对象的类型。
多态:同一个行为具有多个不同表现形式或形态的能力。
运行时类型信息使得你可以在程序运行时发现和使用类型信息。将我们从智能在编译期执行面向类型的操作禁锢中解脱出来。
运行时识别对象和类的信息方式有二:
- RTTI.RTTI假设我们在编译时已经知道了所以的类型。
- 反射机制。反射允许我们在运行时发现和使用类的信息。
面向对象编程的基本目的:让代码只操作对基类的引用。这样如果要添加新类扩展程序变不会影响原代码(多态)。我们希望大部分代码都用多态调用,尽可能少地了解对象的具体类型,使得代码更容易读、写和维护;设计更容易实现、理解和改变。(多态时,虽然我们在写代码时只让这个类或对象表现出其父类的类型,但是在运行时这个对象一直是他该有的具体类型,只不过是他不停的换着面具表示他是这一类,那一类,甚至是Object(God))
我们对抽象的基类用抽象类或者接口表示可以防止对实例化无意义的基类实例化。
Class对象
要理解RTTI在java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作由特殊对象——Class对象来完成,它包含了与类有关的信息。
Class对象就是用来创建类的所以的“常规”(程序要运行使用的)对象的。java使用Class对象来执行其RTTI,像转型这样的操作也是。每个类就是一个Class对象,经过虚拟机编译后就是个 类名.class文件,通过类加载器加载。
所以类都是在对其第一次使用时才动态加载到JVM中的。第一次使用指程序创建第一个对类的静态成员的引用时(构造器也是类的静态方法,因此在使用new创建类的新对象时也是对类的静态成员的引用),就会加载这个类。
如果尚未加载,默认的类加载器就会根据类名查找.class文件(也可能会在数据库中查找字节码)。
使用newInstance()创建的类,必须带有默认的构造器。
//不带默认构造器会报实例化异常
java.lang.InstantiationException: com.fcar.thinkjava.duotai.Sandwich
at java.lang.Class.newInstance(Class.java:364)
at com.fcar.guava.GuavaTest.classTest(GuavaTest.java:385)
生成Class对象的引用方式:
Class<?> name = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
类初始化步骤:
- 加载。通过类加载器查找字节码创建Class对象。
- 链接。验证字节码,为静态域分配存储空间,如有需要将类的符合引用替换为直接引用。
- 初始化。如果有父类,先对父类初始化,执行静态初始化器(包括构造器)和静态初始化块(static 修饰的代码块或域)
Java初始化的原则是尽可能的“惰性”。
仅使用.class语法来获得对类的引用不会引发初始化。
编译器常量的读取不需要对类进行初始化。
如果是一个被static final 修饰的值(编译器常量),那么用类调用读取值不需要对类进行初始化(因为在解析阶段就会把常量的值直接赋值给常量引用)(该类的static代码块什么的不会执行,因为没有初始化)。但是如果被static final 修饰的值 不是编译器常量(如值是通过random调用获得的,那么读取该值还是会执行初始化)。如果只是static修饰(没有final),则肯定要执行初始化。
泛化的Class引用
Class引用总是指向某个Class对象,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。
通过使用泛型语法,可以让编译器强制执行额外的类型检查。(否则普通的类引用是可以被重新赋值的)
通配符 “?” 表示“任何事物”。
//表示获取的是Cycle的父类的类引用
Class<? Super Cycle> = Cycle.getSuperClass();
//表示获取的是Cycle的子类的类引用
Class<? extend Cycle> = someClass;
//转型方式有2(也叫向下转型):
Class<Cycle> cycleT = Cycle.class;
//1
Cycle c = cycleT.cast(a);
//2 直接强转,一般都这样
(Cycle)a;
RTTI形式包括:
- 传统的类型转换。如(Shape)cycle。
- 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
- 关键字 instanceof 。用于判断是否特定类型的实例。
instanceof 只可与命名类型进行比较,而不能与Class对象作比较。注意:如果程序中编写了许多的instanceof 表达式,就说明设计存在瑕疵。
Class<?> name = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
Object o1 = name.newInstance();
//name.isInstance等同于instanceof
System.out.println("isInstance="+name.isInstance(o1));
构造器就算一个工厂方法模式。
P364 工厂方法实例待实践。
instanceof 与Class的等价性
instanceof 与isInstance()保持了类型的概念,指的是你是这个类或者这个类的子类么?
而用==或equal()比较时不考虑继承,比较的是确切的类型是否相同。
反射:运行时类信息
参考
Class类与java.lang.reflect(Field,Method,Constructor)类库一起对反射的概念进行了支持。
通过反射,我们可以使用Constructor创建新的对象,用get()和set()方法(调用Method的方法)读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。还可以调用Class.getMethods(),getFields() ,getConstructors()返回表示方法、字段以及构造器的对象的数组。
RTTI与反射的真正区别只在于:对于RTTI,编译器在编译时打开和检查.class文件;而对于反射机制,编译器在运行时打开和检查.class文件(因为编译时不可获取);
反射在java中是用来支持其他特性的,例如对象序列化和JavaBean。
运行时才获取类信息的动机:
- 需要获取一个指向某个并不在你的程序空间中的对象的引用(如磁盘文件、网络连接中)。
- 希望提供在跨网络的远程平台上创建和运行对象的能力(远程方法调用RMI)。
反射测试类:
@Test
public void reflectTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================获取所有公用的构造方法==============================");
Constructor<?>[] constructors = clazz.getConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println( constructors[i]);
}
System.out.println("===========================获取所有的构造方法==============================");
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (int i = 0; i < declaredConstructors.length; i++) {
System.out.println( declaredConstructors[i]);
}
System.out.println("=============================获取公有 & 有参的构造方法============================");
Constructor<?> constructor = clazz.getConstructor(String.class);
System.out.println(constructor);
System.out.println("=============================获取私有 & 有参 构造方法============================");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Integer.class);
System.out.println(declaredConstructor);
System.out.println("=============================获取公有 & 无参的构造方法============================");
Constructor<?> constructor1 = clazz.getConstructor(null);
System.out.println(constructor1);
System.out.println("=============================构建有参构造器对象============================");
Object o = constructor.newInstance("constructorTest");
System.out.println(o);
}
@Test
public void reflectFieldTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================获取所有公用的域==============================");
Field[] fields = clazz.getFields();
for (int i = 0; i < fields.length; i++) {
System.out.println( fields[i]);
}
System.out.println("===========================获取所有的域==============================");
Field[] declaredFields = clazz.getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++) {
System.out.println( declaredFields[i]);
}
System.out.println("=====获取公有字段并使用=====");
Object instance = clazz.getConstructor().newInstance();
Field publicField = clazz.getField("publicField");
publicField.set(instance,"publicTest");
//有个疑问?如果我们代码里没有这个类,那么又如何强转调用方法和域呢?
ReflectTest reflectTest = (ReflectTest)instance;
//获取字段名称
//直接get
Object publicField1 = publicField.get(instance);
System.out.println("publicField.get+++"+publicField1);
//通过多态获取
System.out.println( reflectTest.getPublicField());
GetSet getSet1 = (GetSet)instance;
System.out.println("=====通过多态+反射获取域值=====");
System.out.println(getSet1.getField(clazz,publicField,instance));
System.out.println("=====获取私有字段并使用=====");
Field privateField = clazz.getDeclaredField("privateField");
//设置私有域要暴利反射
//在获取私有属性的时候如果没有进行暴力反射,那么会抛出异常。
//java.lang.IllegalAccessException: Class com.fcar.guava.GuavaTest can not access a member of class com.fcar.thinkjava.type.ReflectTest with modifiers "private"
privateField.setAccessible(true);
System.out.println("====before====="+reflectTest.getPrivateField());
privateField.set(instance,"privateTest");
System.out.println( reflectTest.getPrivateField());
}
@Test
public void reflectMethodTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================获取所有公用的方法==============================");
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println( methods[i]);
}
//否则会走下去
Thread.sleep(2000);
System.out.println("===========================获取所有的方法==============================");
Method[] declaredMethods = clazz.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
System.out.println( declaredMethods[i]);
}
Thread.sleep(5000);
System.err.println("======获取特定方法(带参)并使用=====");
Object instance = clazz.getConstructor().newInstance();
Method commonActionWithParam = clazz.getMethod("commonActionWithParam", String.class);
System.out.println(commonActionWithParam);
commonActionWithParam.invoke(instance,"METHODPARAM");
Thread.sleep(2000);
System.err.println("======获取特定方法(不带参)并使用=====");
Method commonAction = clazz.getMethod("commonAction");
System.out.println(commonAction);
commonAction.invoke(instance);
Thread.sleep(2000);
System.err.println("======获取私有方法(不带参)并使用=====");
//Method privateAction = clazz.getMethod("privateAction");
Method privateAction = clazz.getDeclaredMethod("privateAction");
//调用私有方法一定要设置权限
privateAction.setAccessible(true);
System.out.println(privateAction);
privateAction.invoke(instance);
}
@Test
public void reflectStaticTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================反射执行STATIC方法==============================");
Method staticAction = clazz.getMethod("staticAction");
staticAction.invoke(null);
}
@Test
public void reflectMainTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.guava.GuavaTest");
System.out.println("===========================反射执行main==============================");
Method main = clazz.getMethod("main", String[].class);
//Object o = clazz.getConstructor().newInstance();
//main 是static方法不需要创建对象
main.invoke(null,(Object) new String[]{"A"});
}
public static void main(String[] args) {
System.out.println("I AM MAIN");
}
动态代理
参考文章2包含cglib
使用场景:
你希望跟踪目标对象中的方法的调用,或者希望度量这些调用的开销,这些代码并不希望将它们合并到应用的代码中,因此代理使得你可以很容易的添加或移除它们。
在Spring项目中用的注解,例如依赖注入的@Bean、@Autowired,事务注解@Transactional等都有用到,换言之就是Spring的AOP(面向切面编程时)。
举个例子:为什么@Transactional 注解只有public权限的才能生效呢?
A:因为@Transactional注解使用时是通体Spring的AOP(面向切面编程)实现的,这时候就用到了动态代理,而动态代理时因为生成的动态代理类实际上是个实现了被代理类的接口的类,而接口中的方法默认都是public的,所以是不可能有其他访问权限的方法生效的,因为动态代理代理不到这个方法。
动态代理的好处是比较灵活,可以在运行的时候才切入改变类的方法,而不需要预先定义它。
JDK动态代理:
@Test
public void dynamicProxyTest() {
TeaMan teaMan = new WuYiShanTeaMan();
InvocationHandler guangZhou13Hang = new GuangZhou13Hang(teaMan);
TeaMan guangZhou13HangProxy =(TeaMan) Proxy.newProxyInstance(teaMan.getClass().getClassLoader(), teaMan.getClass().getInterfaces(), guangZhou13Hang);
guangZhou13HangProxy.sellTea(5000);
guangZhou13HangProxy.buyOpium(3000);
}
public class WuYiShanTeaMan implements TeaMan {
@Override
public void sellTea(Integer hearvy) {
System.out.println("WuYiShanTeaMan sellTea "+hearvy+" KG");
}
@Override
public void buyOpium(Integer hearvy) {
System.out.println("WuYiShanTeaMan buyOpium "+hearvy+" KG, what a shame");
}
}
//接口
public interface TeaMan {
public void sellTea(Integer hearvy);
public void buyOpium(Integer hearvy);
}
//实现InvocationHandler
public class GuangZhou13Hang implements InvocationHandler {
private TeaMan teaMan;
public GuangZhou13Hang(TeaMan teaMan) {
this.teaMan = teaMan;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("===================before====================");
Object invoke;
if (method.getName().equals("buyOpium")) {
System.out.println("官府收账款放行鸦片");
invoke = method.invoke(teaMan, args);
System.out.println("官老爷逛窑子");
} else {
invoke = method.invoke(teaMan, args);
}
System.out.println("===================after====================");
return invoke;
}
}
cglib动态代理:JDK的动态代理一定要继承一个接口,如果要基于POJO类的动态代理 ,那么可以用cglib。
@Test
public void cglibDynamicProxyTest() {
System.setProperty( DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"F:\\mywork\\fc-starter");
AmericanBusinessMan businessMan = new AmericanBusinessMan();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(EastIndiaCompany.class);
enhancer.setCallback(businessMan);
EastIndiaCompany o = (EastIndiaCompany)enhancer.create();
o.buyTea(10000);
o.plantOpium(30000);
}
//代理要实现MethodInterceptor
public class AmericanBusinessMan implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("===================before Opium====================");
Object invoke;
if (method.getName().equals("plantOpium")) {
System.out.println("India MI FANS IN DANGER");
invoke = methodProxy.invokeSuper(o,objects);
System.out.println("india MI FANS GOODBYE");
} else {
invoke = methodProxy.invokeSuper(o, objects);
}
System.out.println("===================after Opium====================");
return invoke;
}
}
//被代理类
public class EastIndiaCompany {
public void buyTea(Integer hearvy) {
System.out.println("EastIndiaCompany buyTea "+hearvy+" KG");
}
public void plantOpium(Integer hearvy) {
System.out.println("EastIndiaCompany plantOpium "+hearvy+" AREA");
}
}
空对象
引入空对象后,我们可以假设所有的对象都是有效的,而不必浪费编程精力去检查null。
2 疑问
以下的报错实际上是因为我们用Class.forName()后并没有创建对象,这时候报错是因为对象还没有创建,getClass()要用实例去调用才能保证该class已经加装到虚拟机中了。
//成功
GuavaTest test = new GuavaTest();
Class instance1 = test.getClass();
Object o = instance1.newInstance();
//报错
Class<?> name2 = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
Class instance2 = name.getClass();
Object o2 = instance2.newInstance();
java.lang.IllegalAccessException: Can not call newInstance() on the Class for java.lang.Class
at java.lang.Class.newInstance(Class.java:344)
at com.fcar.guava.GuavaTest.classTest(GuavaTest.java:390)
为什么1.7版本通过 Class.forName获取类的引用不会立即进行初始化了呢(编程思想里的会。)?
Class<?> initable3 = Class.forName("com.fcar.thinkjava.type.Initable");
在什么情况下我们喜欢用反射?(毕竟自己拼接方法来调用乏味费时)
动态获取对象或者创建对象时。
有个疑问?如果我们代码里没有这个类,那么又如何强转调用方法和域呢?
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
Object instance = clazz.getConstructor().newInstance();
Field publicField = clazz.getField("publicField");
publicField.set(instance,"publicTest");
//有个疑问?如果我们代码里没有这个类,那么又如何强转调用方法和域呢?
ReflectTest reflectTest = (ReflectTest)instance;
//获取字段名称
//System.out.println( publicField.getName());
System.out.println( reflectTest.getPublicField());
A:如果我们代码里没有这个类,那么我们可以约定一个基类接口,定义get/set域方法,让我们从远处,比如说数据库获取的类实现这个基类,这样我们就可以在获取域值时,只强转为基类接口类型,然后通过多态加反射来拼接get方法来获得域的值。
public class ReflectTest implements GetSet{
@Override
public Object getField(Class clazz, Field publicField, Object instance) throws InvocationTargetException, IllegalAccessException {
Method commonAction = null;
try {
commonAction = clazz.getMethod("get"+(publicField.getName().substring(0, 1).toUpperCase() + publicField.getName().substring(1)));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return commonAction.invoke(instance);
}
public interface GetSet {
public void setField(Field publicField, Object val);
public Object getField(Class clazz,Field publicField, Object instance) throws InvocationTargetException, IllegalAccessException;
}
//調用
GetSet getSet1 = (GetSet)instance;
System.out.println("=====通过多态+反射获取域值=====");
System.out.println(getSet1.getField(clazz,publicField,instance));
多态与RTTI的区别和联系?面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。这句话怎么理解?
A:我的理解是多态其实就是通过RTTI来实现的,只是多态是基于父类类型来调用子类的具体方法,可是多态是不能调用子类的扩展方法的,因为发现不到;但如果通过RTTI判断出其子类类型,便能强转为子类类型,也就可以调用子类相对于父类的扩展方法了。
现实代码中很少使用空对象?最多便是google的通过Optional判空,空对象用处不大?
null容易导致空指针,这要求被调用方要写非常多的判空代码,如果没有判空容易造成NPE.
3 思想总结
所有的类型修饰符修饰的方法和域、构造器都可以被反射访问到。但是final域在遭遇修改时是安全的。它不会发生任何修改。
RTTI允许通过匿名基类的引用来发现类型信息。
面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。
如果使用多态时基类未包含我们想要的方法,那么我们可以使用RTTI,继承一个新类,添加自己需要的方法。
反射这一块与动态代理在项目框架中用得比较多,需要熟悉使用。
十五、泛型
1 知识
理解了边界所在,你才能成为程序高手。因为只有知道了某个技术不能做到什么,你才能更好地做到所能做的。
Java泛型的优点与局限:
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类(可以是基类或接口)。如果要编写可以应用于多种类型的代码,可以使用泛型。
一个类,如果我们在需要说明类型的地方都使用基类或接口,这样确实能够具备更好的灵活性,但也会有一些性能损耗;并且即使使用了接口,对程序的约束还是太强,因为这要求写的代码必须使用特定的接口。而我们通过使用泛型,可以是代码能够应用于“某种不具体的类型(在用该类时传进来类型)。
泛型实现了参数化类型的概念。使代码可以应用于多种类型。
使用场景:用于创建容器类。
JAVA泛型的核心概念:告诉编译器想要使用什么类型,然后编译器帮你处理一切细节。
@Test
public void typeTest() {
Holder<String> holder = new Holder<>("I","LOVE","U");
System.out.println(holder);
holder.setFirst("she");
holder.setSecond("is");
holder.setThird("mine");
System.out.println(holder);
Holder<Integer> intHolder = new Holder<>(1,2,3);
System.out.println(intHolder);
}
//泛型类
public class Holder<T> {
private T first;
private T second;
private T third;
public Holder(T first, T second, T third) {
this.first = first;
this.second = second;
this.third = third;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
public T getThird() {
return third;
}
public void setThird(T third) {
this.third = third;
}
@Override
public String toString() {
return "Holder{" +
"first=" + first +
", second=" + second +
", third=" + third +
'}';
}
}
元组类库
元组:它是将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(也称数据传送对象、信使。不允许修改是因为属性值被final修饰)。
利用继承机制可以实现长度更长的元组。
@Test
public void tupleTest() {
Map<String,String> map= new HashMap<String,String>();
map.put("where","home");
ThreeTuple<String,Integer,Map<String,String>> threeTuple = new ThreeTuple<>("baobao",40,map);
System.out.println(threeTuple.toString());
System.out.println(threeTuple.third);
//threeTuple.first = "xiaobao";
}
//二维元组
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A first, B second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "TwoTuple{" +
"first=" + first +
", second=" + second +
'}';
}
}
//三维元组继承自二维
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A first, B second, C third) {
super(first, second);
this.third = third;
}
@Override
public String toString() {
return "ThreeTuple{" +
"third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
泛型接口
泛型接口就是对接口进行泛型,等实现类实现接口时传入想要的类型即可。
public interface Generator<T> {
T getNext();
}
public class CoffeeGenerator implements Generator<Coffee> {
}
泛型方法
我们可以在类中包含参数化方法(参数化方法的意思就是使用泛型),而这个方法所在的类可以是泛型类,也可以不是。是否拥有泛型方法,与其所在的类是否泛型没有关系。
泛型方法使得该方法能够独立于类而产生变化。
基本指导规则:无论何时,只有你能做到,就应该尽量使用泛型方法。如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为更清楚明白。
使用泛型方法不要指明参数类型,因为编译器会进行类型参数推断。(像可以无数次重载的方法)。如果传入的是基本类型,那么自动包装机制会介入自动包装。
可变参数与泛型方法
泛型方法与可变参数列表能够很好地并存。
参考 Arrays.asList(); 方法
@Test
public void TypeTest1() {
List<String> a = makeList("A");
System.out.println(a);
//Serializable[3]@750 类型不同则泛型会找出他们通用的接口或父类作为泛型容器类型
System.out.println(makeList(1,2,"3"));
System.out.println(makeList("A","B","C"));
System.out.println(makeList("I LOVE MORTY,AND I HOPE MORTY LOVE ME".split("")));
}
public static<T> List<T> makeList(T ... args){
List<T> list = new ArrayList<>();
for (T t : args) {
list.add(t);
}
return list;
}
泛型还可以应用于内部类以及匿名内部类。
构建复杂模型
泛型的一个重要好处是能够简单而安全地创建复杂的模型。
泛型擦除
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
Java泛型是使用擦除来实现的。这意味着在使用泛型时,具体的类型信息都被擦除了,List 都被擦除成原生类型List。
当我们希望代码能够跨多个类工作时,使用泛型才有所帮助。
@Test
public void TypeTest2() {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
manipulator.simulator();
}
//普通对象
public class HasF {
public void f(){
System.out.println("HasF f()");
}
}
//泛型类。边界<T extends HasF> 声明T 必须具有类型HasF或者是其子类,这时候我们便能使用obj.f();方法了。
//
public class Manipulator<T extends HasF> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void simulator(){
obj.f();
}
}
java泛型的缺点在于用擦除实现泛型:
擦除减少了泛型的泛化性,如果不使用擦除来实现,而是使用具体化,使类型参数保持为第一类实体,那么他将能够在类型参数上执行基于类型的语言操作和反射操作。
泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。如List将被擦除为List(即容器类型的被擦除为List), 普通的类型变量在未指定边界的情况下被擦除为Object.
擦除的核心动机是为了“迁移的兼容性”,使得泛化的客户端可以用非泛化的类库来使用。
用擦除来实现java泛型的优缺点:
优点是兼容非泛化代码,使我们可以从容的把非泛化代码转变为泛化代码。
缺点是擦除实现的泛型参数类型信息丢失,不能用于显示地引用运行时类型的操作之中,如转型、instanceof操作和new表达式。
擦除和迁移兼容性意味着使用泛型并不是强制的。
P412对于泛型类型的反编译可以看到,两种写法是一样的。
泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。
擦除的补偿
new T()无法实现的原因:一是因为泛型的类型擦除;二是因为编译器不能验证T具有默认(无参)构造器。
java解决无法创建泛型实例( 无法 new T() )的方案是传递一个工厂对象,使用它来创建新的实例。最便利的工厂对象就是Class对象,使用类型标签(构造器使用 Class clazz),就可以使用clazz.newInstance()创建新对象。这种要求创建的对象必须有默认的构造器,否则会运行时异常。要避开这种异常可以选用显式的工厂,限制其类型,使得只能接受实现了这个工厂的类,这样就不会有缺少默认构造器问题。
P414 工厂的选用。
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
//因为我们不能声明
T[] array = new T[sz];
//只能强转
T[] array = (T[])new Object[sz];
因为有了擦除,数组的运行时类型就只能是Object[].如果我们立即将其转型为T[],那么在编译器该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最后是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。
边界
设置边界的目的是为了可以按照自己的边界类型来调用方法,而不是只能调用Objcet的方法。
java使用extends关键字来限制泛型的边界。
实例:
public class EpicBattle {
static <POWER extends SuperHearing> void useSuperHearing(SuperHero<POWER> hero){
hero.getPower().hearSubtleNoises();
}
static <power extends SuperHearing & SuperSmell> void useFind(SuperHero<power> hero){
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogBoy dogBoy = new DogBoy();
useSuperHearing(dogBoy);
System.out.println("===================");
useFind(dogBoy);
List<? extends SuperHearing> dogBoyes1;
//<? extends SuperHearing & SuperSmell> 之所以不能编译通过是因为&这个是用于定义泛型时写的,使用泛型时不能这样写。
//List<? extends SuperHearing & SuperSmell> dogBoyes2;
}
}
interface SuperPower{
}
interface XRayVision extends SuperPower{
void seeThroughWalls();
}
interface SuperHearing extends SuperPower{
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower{
void trackBySmell();
}
class SuperHero< POWER extends SuperPower>{
POWER power;
public SuperHero(POWER power) {
this.power = power;
}
POWER getPower(){
return power;
}
}
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER>{
public SuperSleuth(POWER power) {
super(power);
}
void see(){
power.seeThroughWalls();
}
}
class CanineHero<power extends SuperHearing & SuperSmell> extends SuperHero<power>{
public CanineHero(power power) {
super(power);
}
void hear(){
power.hearSubtleNoises();
}
void smell(){
power.trackBySmell();
}
}
class SuperHearSmell implements SuperSmell,SuperHearing{
@Override
public void hearSubtleNoises() {
System.out.println("hearSubtleNoises");
}
@Override
public void trackBySmell() {
System.out.println("trackBySmell");
}
}
class DogBoy extends CanineHero<SuperHearSmell>{
public DogBoy() {
//子类可以是默认构造器,只要调用了父类的构造器即可,表示能构造出父类来
super(new SuperHearSmell());
}
}
通配符
在创建多态数组时,该数组只能存放具体的子类型的数组的类型,如 Fruit[] f = new Apple[10]; 只能存放Apple及其子类的类型,否则虽然编译期可以通过,运行时却会报错。
通配符引用的是明确的类型,因此下面这个例子里(<? extends Fruit>)的通配符意味着该引用没有指定的具体类型。这种情况下该泛型容器设置不能添加Object类型的值。
使用泛型容器必须指定具体的泛型容器类型。
@Test
public void fruitTest() {
List<? extends Fruit> list = new ArrayList<Apple>();
//add (capture<? extends com.fcar.thinkjava.Types.fruit.Fruit>)
//in List cannot be applied to (com.fcar.thinkjava.Types.fruit.Apple)
//list.add(new Apple());
//add (capture<? extends com.fcar.thinkjava.Types.fruit.Fruit>)
//in List cannot be applied to (com.fcar.thinkjava.Types.fruit.Fruit)
//list.add(new Fruit());
//list.add( new Object());
list.add(null);
}
<? extends Fruit> 只能确认get获得的值的有效边界为Fruit类型,不能确认set设置的值,因为这样意味着他可以是任何事物,比如Object也可以设置,因为它是最基本的父类,这样编译器就无法验证“任何事物”的类型安全性,因此无法set。
超类型通配符:<? super T> 或 <? super Apple> 通过超类型通配符使我们可以确定边界是Apple,该容器的存放的元素都是Apple及其子类,因为适配的最大父类类型就是Apple。
无界通配符:<?>
一般用于如声明这段代码是用java的泛型来编写,并不是要用原生类型编写;还有就是在处理多个泛型参数时,通过无界通配符来运行一个参数是任意类型的,而其他参数为特定类型,这种使用场景特别重要。
如Map<String, ?> map = new HashMap<String, ?>;
使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。
泛型问题
任何基本类型都不能作为类型参数,但可以使用基本类型的包装类来作为泛型类型参数。
注意:自动包装机制不能应用于数组。
一个类不能实现同一个泛型接口的两种变体(如一个类实现了该泛型接口又继承了实现了这个泛型接口的类),由于擦除的原因,这两个变体会成为相同的接口。(去掉泛型参数便可以了)
转型和警告
使用带有泛型类型参数的转型或instanceof不会有任何效果。(因为运行时是类型擦除的)。
正确的转型方式是通过泛型类来转型:
List<String> list = List.class.cast(in.readObject());
自限定的类型
?
动态类型安全检查
通过Collections.checkedList()这些工具类方法使得List、Map、Set等这类集合可以安全地存入泛型类型的元素。
@Test
public void fruitTest() {
List<Apple> list1 = new ArrayList<Apple>();
add(list1);//正常运行,只有把Orange从容器取出时才会报错。
List<Apple> apples = Collections.checkedList(new ArrayList<Apple>(), Apple.class);
add(apples);//报错
}
异常
由于检查型异常的缘故,将不能编写出泛化的代码。
混型
混型:值混合多个类的能力,以产生一个可以表示混型中所有类型的类。混型使组装多个类变得简单易行。
一般就是多重继承,在java这边是实现多个接口,然后在创建域创建出接口对应的实现类,在调用中通过代理调用域中实现类的方法,将方法调用转发给恰当的对象。
潜在类型机制
java的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。
java想实现类似潜在类型机制则编译器会强制要求这些类实现某个接口,以便通过泛型方法< ? extends Perform>来实现这种效果。
对前置类型机制的补偿
- 反射。通过反射可以在实现或继承任何接口的情况下调用其方法。
- 将一个方法应用于序列。(能够实现在编译器类型检查)
- 用适配器仿真潜在类型机制。
2 疑问
p390 末端哨兵怎么实现的,看不明白?
无论何时,只有你能做到,就应该尽量使用泛型方法。这是对的么?首先有个效率问题吧,如非需要适应多种不同类型,没有必要这样处理吧?
Java泛型是使用擦除来实现的,怎么理解?是指都编程Object类型?
类型标签?指的是ClassL类型的属性标签么?
工厂对象?指的是用于创建工厂的东西,比如class、字符串?
什么叫显式的工厂?
p416,成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。这句怎么理解?(数据泛型转型需重新看)
15.10通配符这块不知道其具体用处,工作中有用到吗?
p429例子待研究
15.16 潜在类型机制?Python和C++ 是支持潜在类型机制的语言实例。
泛型还需重新理解一遍,不够全面。
3 思想总结
泛型的最大用处便是在使用容器类集合时(如List、Map等)可以设置某种泛型版本的容器,而不会因为存取时向上转型为Object而需要向下强转。但这只是个方便的编译期检查且使用时减少了强转的代码量,实际上由于泛型是后面添加到java中的,导致泛型的实现为了兼容以前非泛型的代码而使用类型擦除来实现泛型,导致java无法在运行时获取真正的类型信息,无法实现更加泛化的代码。
十六、数组
1 知识
数组的基本认识:数组通过整型索引值访问元素,并且数组的尺寸不能改变。
数组为什么特殊
数组与其他种类的容器之间的区别有三方面:
- 效率。在java中,数组是效率最高的存储和随机访问对象引用序列的方式。(数组是个简单的线性序列)代价是数组对象的大小被固定(比如与ArrayList对比,因为ArrayList可以实现自动分配空间,这种弹性开销时其效率比数组低),且在其生命周期中不可改变。
- 类型。在泛型之前,数组可以创建某种具体类型通过编译期检查来防止插入错误类型,而其他容器不行。泛型后都可以了。
- 保存基本类型的能力。在泛型之前,数组可以持有基本类型,而其他容器不行。泛型后都可以了,因为有字段包装机制。
数组和ArrayList之间的相似性是有意设计的,这使得两者之间的切换比较容易。随着泛型和自动包装机制的出现,数组相比于容器的仅有优点便是效率,而一般情况下,容器比数组拥有更多的功能方法,且数组限制更多,所以一般优先使用容器。
数组标识符只是一个引用(数组是个对象),指向在堆中创建的一个真实对象,这个数组对象用以保存指向其他对象的引用。length方法(length是数组的大小,而不是实际保存的元素个数。)和“[]”语法是访问数组对象唯一的方式。
对象数组与基本类型数组的区别是对象数组保存的是引用(默认初始化为null),而基本类型数组直接保存基本类型的值(按各基本类型的值初始化,如布尔型是false,int是0)。
数组中构成矩阵的每个向量都可以具有任意的长度(这被称为粗糙数组)。
数组与泛型(待重新了解)
数组与泛型不能很好地结合,不能实例化具有参数化类型的数组,我们不能创建泛型数组。因为擦除会移除参数类型信息,而数组必须知道它们所持有的确切类型,以强制保证类型安全。但是我们可以参数化数组本身的类型。
一般而言,泛型在类或方法的边界处很有效,而在类或方法的内部,擦除通常会使泛型变得不适用。
//作用十分有限,只能用同一个值填充各个位置,针对对象而言,就是复制一个引用进行填充。
int[] t = new int[5];
Arrays.fill(t,3);
System.out.println(Arrays.toString(t));
Arrays.fill(t,3,5,5);//填充下标3-4的值
System.out.println(Arrays.toString(t));
//复制数组
//注意复制对象数组时,复制的是对象的引用,而不是对象本身的拷贝,是浅复制。
//System.arraycopy(t,0,y,0,y.length);
//参数依次是:源数组,从源数组的什么位置开始复制的偏移量,目标数组,从目标数组的什么位置开始复制的偏移量,复制元素个数
System.arraycopy(t,0,y,0,y.length);
System.out.println(Arrays.toString(y));
数组的比较
//用来比较整个数组,比较数组元素个数和对应位置上的元素是否相等。注意比较的是元素的内容,不是位置。
Arrays.equals(y,t);
数组元素的比较
java有两种方式提供比较功能:
- 实现 java.lang.Comparable接口,重写compareTo()方法。
- 创建一个实现了Comparator接口的单独的类。传入Comparator比较对象。
Arrays.sort(u,Collections.reverseOrder());
针对已排序的数组,可以使用Arrays.binarySearch(t,5);查找元素的位置。如果查不到,返回负值,表示若要保持数组的排序状态次目标元素所应该插入的位置。负值的计算方式:-(插入点)-1
插入点指(以从小到大排序为例),第一个大于查找对象元素在数组中的位置,如果数组中所有的元素都小于要查找的对象,也就是说查找的值是最大的,那么插入点就是a.size().
@Test
public void arrayTest() {
int[] a = {1, 2, 3, 4, 5};
int[] b = a.clone();
int[] c = a;//两个引用指向同一个数组对象
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
System.out.println(a.equals(b));//false
System.out.println(a == b);//false
System.out.println(a.equals(c));//true
System.out.println(a == c);//true
int[][] aa = {{1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}};
//Arrays.deepToString(aa)用于多维数组展示
System.out.println(Arrays.deepToString(aa));
boolean[][][] flagArr = new boolean[3][3][3];
System.out.println(Arrays.deepToString(flagArr));
System.out.println(Arrays.deepToString(fillArray(2, 3, 4)));
int[] t = new int[5];
Arrays.fill(t, 3);
System.out.println(Arrays.toString(t));
Arrays.fill(t, 3, 5, 5);
System.out.println(Arrays.toString(t));
int[] y = new int[5];
//复制数组
System.arraycopy(t, 0, y, 0, y.length);
System.out.println(Arrays.toString(y));
System.out.println(Arrays.equals(y, t));
Arrays.sort(t);
Integer[] u = new Integer[5];
Arrays.fill(u, 6);
u[2] = 7;
u[4] = 1;
Arrays.sort(u, Collections.reverseOrder());
System.out.println(Arrays.toString(u));
System.out.println(Arrays.binarySearch(t, 5));
//如果使用Comparator排序某个对象数组(基本类型数组无法使用Comparator),在使用binarySearch()时必须提供同样的Comparator。
System.out.println(Arrays.binarySearch(u, 1, Collections.reverseOrder()));//4
//因为-1小于所以的值,对于颠倒排序来说,-1就是数据里面最大的,应该是-size-1 = -6
System.out.println(Arrays.binarySearch(u, -1, Collections.reverseOrder()));//-6
}
2 疑问
数组是个简单的线性序列,什么叫线性序列?为什么线性序列的元素访问非常快速?
16.6 忽略了创建测试数据这节。
为什么未排序数组上执行二分法查找结果是不可预知的?我测试结果是可知不变的,不过是错误的。
@Test
public void array1Test() {
int[] a = {1, 2, 3, 4, 5, 8, 1, 3, 2};
//Arrays.sort(a);//7
int i = Arrays.binarySearch(a, 3);
System.out.println(i);
}
3 思想总结
优先选择容器而不是数组,只有当性能成为问题且切换到数组对性能提高有所帮助时,才应该重构成数组。
十七、容器深入研究
针对容器这块有很多疑问,肯定要经过二次过滤学习,所有不懂的可以先记下来跳过。回过头来再看。
1 知识
集合类库图
填充容器
所有的Collection 子类型都有一个接收另外一个Collection 对象的构造器,用所接收的Collection 对象中的元素来填充新的容器。
通过继承Abstract容器类来定制Collection 和Map实现。
享元模式:在普通的解决方案需要过多的对象,或者产生普通对象太占用空间时使用享元。
Collection的功能方法
注意:Collection的方法不包括随机访问的get()方法。因为Collection包含Set,而Set是自己维护内部顺序的。
//List 的 <T> T[] toArray(T[] a);方法用法
String[] array = names().toArray(new String[names.size()]);
String[] array1 = names().toArray(new String[0]);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array1));
//不能强转,要用上面的泛型方法
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
//String[] array2 = (String[]) names().toArray();
// System.out.println(Arrays.toString(array2));
可选操作
最常见的未获支持的操作,都来自于背后由固定尺寸的数据结构支持的容器。
应该把Arrays.asList(array1)的结果作为构造器的参数传递给任何Collection(或者addAll()方法或Collections.addAll()方法), 这样可以生成允许使用所有的方法的普通容器
//以下两个都不支持修改List容器的操作,会抛出UnsupportedOperationException异常
//他们的唯一的区别是Arrays.asList(array1)支持set()方法,因为它不会改变该数组的长度,该List是基于一个固定大小的数组,
// 仅支持那些不会改变数组大小的操作,
// 而Collections.unmodifiableList(new ArrayList<String>()) 是禁止了所以的修改操作,所以不行。
//任何会引起对底层数据结构的尺寸进行修改的方法都会抛出UnsupportedOperationException异常,
List<String> list1 = Arrays.asList(array1);
List<String> list2 = Collections.unmodifiableList(new ArrayList<String>());
List的功能方法
public static void main(String[] args) {
System.out.println(capitals().get("CHINA"));
System.out.println(names());
String[] array = names().toArray(new String[names.size()]);
String[] array1 = names().toArray(new String[0]);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array1));
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
//String[] array2 = (String[]) names().toArray();
// System.out.println(Arrays.toString(array2));
//以下两个都不支持修改List容器的操作,会抛出UnsupportedOperationException异常
//他们的唯一的区别是Arrays.asList(array1)支持set()方法,因为它不会改变该数组的长度,该List是基于一个固定大小的数组,
// 仅支持那些不会改变数组大小的操作,
// 而Collections.unmodifiableList(new ArrayList<String>()) 是禁止了所以的修改操作,所以不行。
//任何会引起对底层数据结构的尺寸进行修改的方法都会抛出UnsupportedOperationException异常,
// 因此,应该把Arrays.asList(array1)的结果作为构造器的参数传递给任何Collection(或者addAll()方法或Collections.addAll()方法,
// 这样可以生成允许使用所有的方法的普通容器)
List<String> list1 = Arrays.asList(array1);
List<String> list2 = Collections.unmodifiableList(new ArrayList<String>());
//判断容器内是否有该元素,有则返回该元素所在第一个位置,没有返回-1
int sudan = list1.indexOf("SUDAN");
//返回最后出现该元素的位置,没有返回-1
int sudan1 = list1.lastIndexOf("SUDAN");
//判断容器是否为空,没有存任何元素,如果容器中添加了null也算是元素,会返回false.
boolean empty = list1.isEmpty();
List<String> list3 = new ArrayList<String>();
list3.add(null);
boolean empty1 = list3.isEmpty();//false
System.out.println(empty);
ListIterator<String> listIterator = list1.listIterator();
//传入index参数则会把游标cursor从0移到参数为3迭代,也就是说只迭代从第四个元素开始的容器元素。
ListIterator<String> listIterator1 = list1.listIterator(3);
System.out.println(listIterator.next());
System.out.println(listIterator1.next());
list3.add(null);
list3.add(null);
//可以通过下标参数移除元素,返回该下标位置上元素的值(从0开始),如果传入的index下标超过
// list长度则会抛IndexOutOfBoundsException
String remove = list3.remove(0);
//也可以通过想要移除的具体元素来移除容器中这个值的元素,如果有则移除返回true,没有返回false,
// 多个相同元素只移除第一个匹配到的元素(一般都是从0到list.size()开始匹配)
boolean b = list3.remove(null);
System.out.println(b);
//修改容器指定位置的值
list3.set(0,"1");
list3.add("2");
list3.add("1");
list3.add("1");
list3.add("5");
ArrayList<String> list4 = new ArrayList<>();
list4.add("1");
//保留list3中两个List的交集
boolean b1 = list3.retainAll(list4);
System.out.println(list3);
//移除list3中所有与list4元素值相同的元素,不论有多少个相同的全部移除。
boolean b2 = list3.removeAll(list4);
System.out.println(list3);
//移除容器中所有的元素,包括null元素。
list3.clear();
System.out.println(list3);
}
Set和存储顺序
自定义对象去重(Set篇)
如果没有其他的限制,默认选择HashSet,因为它对速度进行了优化。
在使用容器时,必须为散列存储和树型存储都创建或者重写一个equals()方法。hashCode()方法只有在这个类被置于HashSet或者LinkedHashSet中时才是必需的。
良好的编程风格要求我们在覆盖equals()方法时,总是同时要覆盖hashCode()方法。
SortedSet (被TreeSet实现)
SortedSet 中的元素可以保证处于排序状态(按对象的比较函数进行排序)。
如果要按插入顺序排序其实就是LinkedHashSet.
@Test
public void sortedSetTest() {
SortedSet<String> set = new TreeSet<>();
Collections.addAll(set,"all man are create equal".split(" "));
System.out.println(set);
System.out.println(set.first());
System.out.println(set.last());
System.out.println(set.comparator());
Iterator<String> iterator = set.iterator();
String low = null;
String high = null;
for (int i = 0 ;i<4;i++){
if (i==1) {
low = iterator.next();
}
if (i==3) {
high = iterator.next();
} else {
iterator.next();
}
}
//截取set,包括low,不包括high
System.out.println(set.subSet(low,high));
//截取set小于high
System.out.println(set.headSet(high));
//截取set大于等于low
System.out.println(set.tailSet(low));
}
队列
LinkedList和PriorityQueue的差异仅在于排序行为而不是性能。
普通队列一般是先进先出的。ArrayBlockingQueue、ConcurrentLinkedQueue、LinkedBlockingQueue、PriorityBlockingQueue这些除了最后一个优先级队列,都是按照元素进入队列的先后顺序拿到的。
双向队列
双向队列还是一个队列,但是我们可以在任何一端添加或移除元素。可以通过LinkedList创建双向队列。
理解Map
映射表(也称关联数组)的基本思想是它维护的是键值对,因此你可以使用键来查找值。
HashMap | TreeMap | LinkedHashMap | WeakHashMap | ConcurrentHashMap | IdentityHashMap .
以上的Map行为特性各个不同,表现在效率、键值对的保存和呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定“键”等价的策略等方面。
性能
当在get()中使用线性搜索(for循环遍历)时,执行速度会相当的慢,HashMap使用了散列码来取代对键的缓慢搜索。散列码是“相对唯一”的,用以代表对象的int值。
Map的键必须是唯一的,而值可以有重复。因此键keySet返回的是Set;而值是Collection。
TreeMap是SortedMap目前的唯一实现,如果没有传或者值没有comparator,则默认是自然排序。键值对是按键的次序排列的。
//accessOrder = TRUE 表示采用基于访问的LRU算法
LinkedHashMap<String,String> map = new LinkedHashMap(16,0.75F,true);
LinkedHashMap 是以插入的顺序进行遍历的,基于LRU算法的版本也是如此。但是,在LRU版本中,访问越少的元素会出现在队列的最前面。
散列与散列码
Object的hashCode()使用对象的地址计算散列码;equals()比较对象的地址。因此,如果要使用自己的类作为HashMap的键,那么就要重写equals()和hashCode()方法。
正确的equals()必须满足以下5个条件:
- 自反性 x.equals(x)=true
- 对称性 y.equals(x)=true→x.equals(y)=true
- 传递性 x.equals(y)=true,y.equals(z)=true→x.equals(z)=true
- 一致性 无论比较对少次,结果都是一样的
- x!=null→x.equals(null)=false
注意:Map.Entry是一个接口,用来描述依赖于实现的结构,因此如果你想要创建自己的Map类型,就必须同时定义Map.Entry的实现。
为速度而散列
线性查询是最慢的查询方式。
对查询速度再进一步的解决方案是保持键的排序状态,然后使用二分法进行查询。Collections.binarySearch()。
散列的价值在于速度,散列使得查询得以快速进行,它将键保存在某处,以便能够很快找到。
存储一组元素最快的数据结构是数组,使用数组来表示键的信息(散列值,作为数组的下标)。
为解决数组容量被固定的问题,不同的键可以产生相同的下标。当键相同时,则比较内容。
查询一个值的过程首先是计算散列码,然后使用散列码查询数组。如果没有冲突,则是个完美的散列函数;如果冲突了,则冲突通过外部链接处理。数组不直接保存值,而是保持值的List,如LinkedList,然后对list值使用equal方法进行线性查询。(虽然线性查询最慢,但是因为散列得好的话,每个位置只有较少的值,这样就不太好影响效率)
散列表的”槽位“(slot)通常称为桶位(bucket)。
为使散列分布均匀,一开始是使用质数,后面发现质数并不是散列桶的理想容量,后面改用2的整数次长度的散列表,用掩码代替除法。求余的%操作是开销最大的操作(因为get()是使用最多的)。
生成hoshcode()方法参考如下:
选择不同的容器实现来使用
容器之间的区别通常归结为由什么样的数据结构来实现它们。
虽然有些容器接口相同会拥有共同的操作,但操作的性能却并不相同,在选择使用哪个时可以基于某个特定操作的频率和执行速度来进行选择,一般用性能测试来选择。
P538的性能测试demo可以学习一下。
结果:使用时最佳的做法是将ArrayList作为默认首选,只有当需要额外的功能,如经常从表中间插入和删除时才去选择LinkedList。
HashSet的性能基本上总是比TreeSet好,优先使用,除了当我们需要一个排好序的set时,才应该使用TreeSet,
TreeSet迭代通常比HashSet要快。插入操作,LinkedHashSet比HashSet代价要高是因为LinkedHashSet还要维护链表(保证插入顺序)。
优先使用HashMap,只有在要求Map始终保持有序时,才需要使用TreeMap。LinkedHashMap比HashMap代价要高是因为LinkedHashMap还要维护链表(保证插入顺序)。
HashMap的性能因子
HashMap使用的默认负载因子是0.75。即尺寸/容量。 尺寸值表中当前存储的项数,容量指表中的桶位数。HashMap可以指定初始容量。
java容器类类库采用快速报错(fail-fast)机制。一旦发现容器上有任何除了当前进行锁进行的操作以外的变化就会抛出ConcurrentModificationException异常。
持有引用
软引用 SoftReference 、弱引用 WeakReference、虚引用 PhantomReference。对象的可获得程度由强到弱。使用这些类为垃圾回收提供了更大的灵活性。
使用前提:想继续持有对某个对象的引用,但也希望能够允许垃圾回收器释放它。这样我们便可以继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。
使用方法:使用Reference对象作为调用者与普通引用之间的包装(代理),且不能有其他普通的引用指向那个对象,这样才能达到上面的目的。(继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。)
如果引用的是个普通的引用,则该对象是可获得的,那么垃圾回收器就不能释放它。
SoftReference 用以实现内存敏哥的高速缓存。
WeakReference 为了实现规范映射而设计的。
PhantomReference 用以调度回收前的清理工作,它比java终止机制更灵活。
注意:使用SoftReference 和 WeakReference 可以选择是否要将它们放入ReferenceQueue队列。(回收前清理工作的工具)。而PhantomReference 只能依赖于ReferenceQueue。
552
2 疑问
我个人认为,容器的深入研究这一章不需要勉强自己第一遍就要看懂,看透,但需要多研究几遍,直到看透了,理解了基本常用的这些容器底层原理。
什么是掩码?
散列机制是如何工作的?
使用散列容器时如何编写hashCode()和equals()方法?
为什么某些容器会有不同版本的实现?如何选择?
17.2.1 的generator?
p494 中LinkedHashSet 的添加操作是不是重复添加了一遍?
容器类的打印是不是都是默认toString就行?
Map.Entry<String, String> 里的这个Entry是个什么?
Map.Entry是一个接口,用来描述依赖于实现的结构,因此如果你想要创建自己的Map类型,就必须同时定义Map.Entry的实现?
内部类在Map等容器里的使用?
Map的内部实现和构造?
P525练习?
17.2重新研究一下
动态语言可以在任何对象上调用任何方法,并且可以在运行时发现某个特定调用是否可以工作。动态语言和静态语言的区别?对比?研究一门动态语言自我感觉与静态语言的对比?
List的内部结构和底层?
什么叫链表,单向链表和双向链表?
list1.listIterator()的使用?
p510练习8.
散列存储和树型存储?指的是如HashSet和TreeSet么?
怎么自己重写equals方法 hashcode方法?怎么用idea重写?
HashMap的底层实现?其他Map的底层实现?
P517下面注释写的好处怎么解释?
使用数组代替溢出桶,有两个好处:
- 可以针对磁盘存储方式做优化。
- 在创建和回收单独的记录时,能节约很多时间。
最近最少使用(LRU)算法?一般用在什么场合?
3 思想总结
待续。。。。。。
其他
roy mustang 原来钢炼里的罗伊.马斯坦 是野马的意思。
面向函数式编程 :Scala 、Elm 。可以多去了解下函数式编程的语言,语言的目标是写出更简洁的代码,或者效率更高,或者你的代码更容易被大家懂 。
语言方面更多关心的议题 :第一,多产。第二,多线程的问题。第三,错误,如何发现跟错误相关的一些议题
每当我有问题需要被解决的时候我发现Python是最快可以给我结果的一个语言 。
为什么Python会作为机器学习非常好的一种语言,因为Python把其他语言做了一个封装,调用其他语言做的包。很多的数据科学家他们其实是不希望学习过于复杂的编程语言,能够把他们关于数据方面处理的智慧进行封装起来,通过Python来调用这样会方便很多,这也是为什么Python这几年这么流行的原因。
mysql排序问题:比如我们以创建日期来排序,当日期相同时,mysql会随机排序。
疑问:
有点意识到自己喜欢理论大而泛的模糊知识的学习,而不喜欢实践和细节的打磨,是因为粗心浮躁导致的么>
在我看来,其实是对知识的空洞和无知,导致了落实不到细节,因此只能宏观上了去感受和学习,落实不到细节上来,还不到粗心浮躁,而是基础太差,只能一步一个脚印,找个清单,从头开始学习起,就不会学习个新东西被俄罗斯套娃到另一个新东西再到另一个新东西上了,因为你懂的足够多,更容易理解这样东西或者技术。
参考文献
JAVA编程思想
建议不继续维护,整理新版本下的Java基础。
- A-Z ↩︎