Ordinary Road


  • 首页

  • 标签

  • 分类

  • 归档

【Zookeeper】Unable to read additional data from client sessionid*,likely client has closed socket

发表于 2018-11-20 | 分类于 问题解决 | 阅读次数:

一、问题描述:

    因为项目中使用到了Zookeeper,所以我自己找了些关于zk的资料学习了一下。在异步创建节点的过程中,抛出了如下问题:

png1

    异步创建节点的时候总是闪退,然后服务端报错 Unable to read additional data from client sessionid 0x162b246bfe50000, likely client has closed socket ,我们先看下代码 这里我把同步跟异步的代码一起贴了出来便于学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package zk.zkTest;
import java.io.IOException;
import java.util.Random;

import org.apache.zookeeper.AsyncCallback.DataCallback;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.KeeperException.NodeExistsException;
import org.apache.zookeeper.data.Stat;
//实现一个监视点Watcher接口
public class Master implements Watcher{

public static final String HOST_POT = "127.0.0.1:2181";

//zk连接对象
public ZooKeeper zk;
//连接地址、端口
public String hostpot;
//构造函数 传入连接参数
Master(String hostpot){
this.hostpot = hostpot;
}
//启动
void zkStart(){
try {
//this传入该类的对象
zk = new ZooKeeper(hostpot,400000,this);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//关闭
void zkStop(){
try {
zk.close();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//创建主节点存储的数据
Random random = new Random();
String serverId = Integer.toHexString(random.nextInt());
//创建主节点(同步)
void runForMasterSyn() throws InterruptedException{
//循环的原因是 当客户端与服务端发生网络中断时 客户端会尝试重新连接服务端
while(true){
try {
// 同步调用方法 第一个参数节点名称 第二个是节点数据 第三个是安全策略 此处使用开放式的ACL访问控制列表 第四个是创建模式 此处创建了一个临时节点
zk.create("/master",serverId.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL);
isLeader = true;
break;
} catch (NodeExistsException e) {
// TODO Auto-generated catch block
e.printStackTrace();
isLeader = false;
break;
} catch (KeeperException e) {
// TODO Auto-generated catch block 网络连接异常 此处不确定主节点是否已经创建完成 网络断开可能发生在请求之前,也可能发生在请求创建之后 因此要考虑这种异常 所以此处不返回 执行下面的check判断
e.printStackTrace();
}
if(checkMasterSyn()){
break;
}
}
}

public static boolean isLeader = false;
//检查主节点是否已经创建 发生上述两种异常时执行 (同步)
boolean checkMasterSyn() throws InterruptedException{
while(true){
Stat stat = new Stat();
try {
//获取节点的数据 第二个字段为是否启动监听后续的变化
byte data[] = zk.getData("/master",false,stat);
//如果数据里的内容是当前的进程存储的内容 则表示当前进程为主节点 即创建成功
isLeader = new String(data).equals(serverId);
return true;
} catch (NoNodeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
} catch (KeeperException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

//创建主节点(异步) 与同步相比多了两个参数 1.回调方法 2.指定的上下文信息 回调方法调用的是传入的对象的实例 (回调要知道是哪个调用的回调)同时因为回调返回之前不需要等待create结果 所以没有Interrupt异常 因为执行结果会在回调之后收到 所以也没有Keeper异常
void runForMasterAsyn(){
zk.create("/master",serverId.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL,masterCreateCallBack,null);
}
//创建回调对象 异步调用只有一个线程处理回调请求 因此遵循FIFO的原则 所以应避免异步调用里处理大量逻辑或者阻塞代码
StringCallback masterCreateCallBack = new StringCallback(){
//回调的函数 rc 返回调用的结果 返回OK或者异常编码 path 对应create的第一个参数 ctx 对应create的上下文 即最后一个字段 name znode节点名 对于非有序节点 path与name相同 有序节点 name会自动标号
public void processResult(int rc, String path, Object ctx,
String name) {
// TODO Auto-generated method stub
switch (Code.get(rc)){
case CONNECTIONLOSS:
checkMasterAsyn();
return;
case OK:
isLeader = true;
break;
default:
isLeader = false;
}
System.out.println("I'm "+(isLeader?"":"not")+" Leader");

}
};
//检查主节点状态 (异步调用创建主节点时做的检查)
void checkMasterAsyn(){
//第一个参数节点名称 第二个参数是否设置监听 第三个为获取数据的异步回调 第四个为上下文信息
zk.getData("/master",false,masterCheckCallBack,null);
}
//异步获取节点数据信息
DataCallback masterCheckCallBack = new DataCallback(){
public void processResult(int rc, String path, Object ctx,
byte[] data, Stat stat) {
// TODO Auto-generated method stub
switch(Code.get(rc)){
case CONNECTIONLOSS:
//此处递归循环调用 尝试连接
checkMasterAsyn();
return;
case NONODE:
//没有节点重新创建
runForMasterAsyn();
return;
}
}

};
//实现的接口方法
public void process(WatchedEvent arg0) {
// TODO Auto-generated method stub
System.out.println(arg0);
}
public static void main(String[] args) throws InterruptedException {
Master m = new Master(HOST_POT);
m.zkStart();
m.runForMasterAsyn();
if(isLeader){
//此处用来处理主节点业务逻辑
System.out.println("I'M THE LEADER");
Thread.sleep(2000000000);
}else{
System.out.println("SORRY,SOMEONE ELSE IS THE LEADER");
}
m.zkStop();
}
}

我们看下运行之后的控制台信息:

png2

二、解决方案:

    从上边控制台的信息可以看出 if/else的判断发生在了异步回调之前,并且打印了最后一行日志Session closed
所以这里抛错的原因是:没有等待异步通知的响应信息,就提前关闭了连接。解决方法就是在if/else之前延时等待,或者设置变量等待异步通知返回结果之后再进行if/else判断。

    网上还有很多关于这个错误的解决方案,场景不同,但大多数的原因都是因为网络中断,有的可能是超时时间不够,我这里的原因是在异步通知返回结果之前就人为的结束了连接。

【Oracle】ORA-06413 连接未打开

发表于 2018-11-20 | 分类于 问题解决 | 阅读次数:

问题描述:

    使用toad连接oracle报错“ORA-06413 连接未打开”

解决方案:

    toad安装路径下存在诸如‘(’等特殊字符,比如我的路径是Program Files(x86),修改安装路径为D:\tools等不带特殊字符的路径即可

【Java编程思想】十五:类型信息

发表于 2018-11-20 | 分类于 学习笔记 | 阅读次数:

一、为什么需要RTTI

    RTTI(Run-Time-Type-Information),运行时类型信息。读《Java编程思想》这本书时是第一次知道这个词,于是先百度了一番,某乎上的讲解是这个概念最早是由本书的作者在《Think in C++》上提出的,实际上的意思与Java中的反射差不多。书中主要说的应用场景是在多态的过程中,基类的派生类使用基类提供的方法时,动态绑定以及向上转型相关,在最后阶段运行过程中找到自己对应的派生类。也正是有了RTTI,编译器才能正确的找到对应的派生类。上述思想是Java中多态思想的体现,多态也是Java中设计的关键思想,尽量使代码尽可能少的了解其具体的类型,而只与通用的类(基类)打交道。这种设计使代码逻辑更加简单、易于理解。

二、Class对象

    要理解RTTI在Java中的工作原理,首先要知道类型信息在运行时是如何表示的。这项工作是由称为Class对象的一种特殊的对象表示的。面向对象思想告诉我们,万事万物都是对象,Java中除了基本的数据类型,其它的都是对象。那么如果我们有如下代码:

1
Student s = new Student();

    我们使用new Student()创建了一个Student类的对象,那么Student类又是不是对象呢?没错,如前文所说,万事万物都是对象,这里Student就是Class的对象,也就是说任何声明定义的类,都是Class类的类对象。当编译器编译完一个类时,在同名的.class文件中就会保存这个类的Class对象。而为了生成这个对象,运行这个类的JVM会使用一个称作是“类加载器”的子系统。

    类加载器子系统实际上可以包含一条类加载器链,但只有一个原生的类加载器,它是JVM实现的一部分,用来加载所谓的可信类,以及JavaAPI。如果还需要其它特殊的操作,比如支持Web服务应用,或者是在网络上下载类,那么就可以在类加载器链上挂载额外的类加载器。

    所有的类在其第一次加载的时候,都被动态的加载到JVM中,当程序创建第一个类的静态成员引用时,就会被加载。这也说明类的构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此使用new关键词创建的类的新的对象,也会被当做是类的静态成员引用。也正是如此,Java程序在运行之前并非完全加载,动态加载在诸如C++这类的静态加载语言是无法实现的。

一旦某个类的Class对象被载入内存,它就用来创建这个类的所有对象,如下示例说明这点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.chenxyt.java.practice;
class Candy{
static{
System.out.println("Loading Candy");
}
}
class Gum{
static{
System.out.println("Loading Gum");
}
}
class Cookie{
static{
System.out.println("Loading Cookie");
}
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("Inside main");
new Candy();
System.out.println("After Creating Candy");
try{
Class.forName("com.chenxyt.java.practice.Gum");
}catch(ClassNotFoundException e){
System.err.println("Gum Not found");
}
System.out.println("After Creating Gum");
new Cookie();
System.out.println("After Creating Cookie");
}
}

运行结果:

png1

    可以看到Class对象仅在需要的时候被加载,static初始化是在类加载时候进行的。需要注意的是Class.forName()这个方法,它是Class类的一个静态方法,所有的类都是Class类的对象,forName()是取得Class对象引用的一种方法。它使用目标类名作为参数,返回Class对象的引用。如果找不到指定的类名则抛出ClassNotFound异常。无论何时你想获取到类的运行时使用的类型信息,就必须要获得恰当的Class对象的引用,Class.forName()是实现此功能的捷径。因此你不需要为了获得Class对象的引用而持有该类型的对象。但是如果你已经拥有了一个指定类型的对象,那么就可以使用.getClass方法获取Class对象的引用。这个方法属于Object,它将返回表示该对象实际类型的Class对象引用。

Class还有很多实用的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.chenxyt.java.practice;
interface HasBatteries{

}
interface WaterProof{

}
interface Shoots{

}
class Toy{
Toy(){

}
Toy(int i){

}
}
class FancyToy extends Toy implements HasBatteries,WaterProof,Shoots{
FancyToy() {
super(1);
}
}
public class ToyTest {
static void printInfo(Class cc){
System.out.println("Class Name:" + cc.getName() + " Is Interface?" + cc.isInterface());
System.out.println("Simple Name:" + cc.getSimpleName());
System.out.println("Canonical Name:" + cc.getCanonicalName());
System.out.println("----------");
}
public static void main(String[] args) {
Class c = null;
try{
//获取指定类的类对象引用
c = Class.forName("com.chenxyt.java.practice.FancyToy");
}catch(ClassNotFoundException e){
System.err.println("Class Not Found");
System.exit(1);
}
printInfo(c);
//获取该类实现的接口
for(Class face:c.getInterfaces()){
printInfo(face);
}
//获取该类的基类
Class up = c.getSuperclass();
Object obj = null;
try{
//Class类的构造器 用来构造不确定类型的对象
obj = up.newInstance();
}catch(InstantiationException e1){
System.err.println(e1);
}catch(IllegalAccessException e2){
System.err.println(e2);
}
printInfo(obj.getClass());
}
}

运行结果:

png2

    FancyToy继承了Toy并实现了三个接口,使用Class.forName()方法创建一个FancyToy类的类对象,并指向引用c。getName()产生全类名,getSimpleName()获取不带包路径的类名,getCanonicalName()获取全类名。isInterface()判断当前类的类型是不是接口,Class.getInterfaces()返回当前类的全部接口。如果你已经有了一个Class对象,还可以使用getSuperClass()获取这个类的基类。Class的newInstance()方法是实现了一个“虚拟的构造器”,意思是我不知道你的确切类型,但是你必须要正确的进行构造。使用newInstance()创建的类,必须要带有默认构造器。

    Java中除了以上两种Class.forName()和obj.getClass()可以产生类的对象引用,还有一种类字面常量的形式:

1
FancyToy.class:

    这样做不仅简单,而且更加安全,因为在编译时即可检查,无需使用try-catch语句,所以更加高效。类字面常量不仅可以应用与普通的类,还可以应用在基本数据类型,以及接口和数组中。此外对于基本数据类型的包装器类,它还有个TYPE字段,TYPE是一个引用,指向对应的基本数据类型的Class对象。

    此处有一点非常重要需要注意,当使用.class来创建对象的引用时,不会自动的初始化该Class对象。为了使用类而做的准备工作实际上包含三个步骤:
1.加载:这是由类加载器执行的,该步骤将查找字节码(通常在classpath路径查找,但不是必须的),并从这些字节码中创建一个Class对象;
2.链接:在链接阶段验证类的字节码,为静态域分配存储空间,并且如果必要的话,会解析这个类创建的对其它类的所有引用;
3.初始化:如果该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化块。
初始化被延迟到了对静态方法(构造器也是隐式的静态方法)或者非常数静态域进行首次引用时才执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.chenxyt.java.practice;
import java.util.Random;
class Initable{
 static final int staticFinal = 45;
 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
 static{
  System.out.println("Initializing Initable");
  System.out.println("Initable---------------");
 }
}
class Initable2{
 static int staticNonFinal = 145;
 static{
  System.out.println("Initializing Initable2");
  System.out.println("Initable2---------------");
 }
}
class Initable3{
 static int staticNonFinal = 54;
 static{
  System.out.println("Initializing Initable3");
  System.out.println("Initable3---------------");
 }
}
public class ClassInitialization {
 public static Random rand = new Random(45);
 public static void main(String[] args) throws ClassNotFoundException {
  Class initable = Initable.class;
  System.out.println("After Creating Initable Ref");
  System.out.println(Initable.staticFinal);
  System.out.println("------------------");
  System.out.println(Initable.staticFinal2);
  System.out.println(Initable2.staticNonFinal);
  Class initable3 = Class.forName("com.chenxyt.java.practice.Initable3");
  System.out.println("After Creating Initable3 Ref");
  System.out.println(Initable3.staticNonFinal);
 }
}

运行结果:

png3

    从结果中可以看出.class并不会引发初始化,相反使用Class.forName()时则立刻完成了初始化的功能。同时,如果一个域被声明为static final的编译时常量,那么它可以不初始化类就被访问,就像Initable.staticFinal一样,但是仅仅被声明为static final并不会保证类不被初始化就能访问,就像Intable.staticFinal2那样,因为它不是编译时常量。
    如果一个static域不是final的,那么在对它访问的时候,总是要求在读取之前先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像在对Initable2.staticNonFinal的访问中看到这样。
Class类允许使用泛型指定相应的类型,如下两种形式均可:

1
2
3
4
5
6
7
8
9
10
11
12
package com.chenxyt.java.practice;
public class GenericClassReference {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class;
intClass = double.class;
//类型不匹配
// genericIntClass = double.class;

}
}

    普通的类引用不会产生警告信息,并可以被重新指定为其它类型的引用。而泛型类的引用只能被指定为指定的类型。通过使用泛型语法,可以使编译器做一些额外的类型检查。如果我们希望放松这种限制,那么似乎可以使用:

1
Class<Number> genericClass = int.class;

    因为Integer类继承自Number类,但实际上并不可以正常工作,因为Integer Class对象不是Number Class对象的子类。为了放松这种类型的限制,我们使用了通配符,它是Java泛型的一部分。通配符就是“?”表示任何事物。

1
Class<?> intClass = int.class:

    尽管看起来这种形式与直接使用Class相同,但是它不会产生编译器警告信息。更确切的说使用通配符表示的是你并非是碰巧或者是由于疏忽而选择了一个非具体类的引用,而是你就是明确的选择了一个非具体的版本。使用通配符与extends关键字,可以有效的解决前边所说的继承的问题:

1
Class<? extends Number> genericClass = int.class;

    表示创建一个Number 或是它子类的类对象。

三、类型转换器先做检查

    目前的类型转换信息有两种,一种是传统的类型转换,由RTTI确保类型转换,如果执行了一个错误的类型转换,那么将抛出一个ClassCastException异常,另一种是代表对象类类型的Class对象,通过查询Class对象的信息获取运行时的所有状态。RTTI在Java中还有第三种类型转换的形式,就是使用关键字instanceof,它返回布尔值,判断某个对象是不是某个特定类的实例。

1
2
3
if(x instanceof Dog){
(Dog)x.bark();
}

    这种先进行判断然后再使用的方式显得很必然,比如在进行向下转型的时候,如果不先进行判断,很容易发生ClassCastException异常。此外也提供了动态判断的形式isInstanceof()方法。

四、注册工厂

    使用注册工厂的目的是将对象的创建工作交给类去完成,即创建一个工厂方法,然后进行多态的调用,从而为你创建恰当类型的对象。在如下的简单的版本中,工厂方法就是Factory接口中的Create方法。

1
public interface Factory<T>{T create();}

    所谓工厂方法,就是意味着我只提供一个创建实例的工厂方法,而无需每创建一个继承类就编写一个新的方法。关于工厂方法更多细节后边在设计模式模块专门再进行学习。

五、instanceof与Class的等价性

    在查询类型信息的时候,instanceof与直接比较Class对象有一点很重要的区别,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.chenxyt.java.practice;
class BaseType{

}
class Derived extends BaseType{

}
public class FamilyVsExactType {
static void test(Object x){
System.out.println("Testing x of type" + x.getClass());
System.out.println("x instance of BaseType" + (x instanceof BaseType));
System.out.println("x instance of Derived" + (x instanceof Derived));
System.out.println("BaseType.isInstance(x)" + BaseType.class.isInstance(x));
System.out.println("Derived isInstance(x)" + Derived.class.isInstance(x));
System.out.println("x.getClass == BaseType.class" + (x.getClass() == BaseType.class));
System.out.println("x.getClass == Derived.class" + (x.getClass() == Derived.class));
System.out.println("x.getClass.equals(BaseType.class)" + (x.getClass().equals(BaseType.class)));
System.out.println("x.getClass.equals(Derived.class)" + (x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new BaseType());
test(new Derived());
}
}

运行结果:

png4

    可以看出,instanceof表示的是“你是这个类吗?或者你是这个类的子类吗?”而使用Class对象进行比较时,表示的则只有明确的类型信息,忽略了继承的关系。

六、反射:运行时类型信息

    我理解的反射概念是程序在编译的时候并不知道具体的类型信息,直到程序运行时通过反射才获取到了准确的类信息。这里提供了几个方法支持获取准确的类信息,以便创建动态的代码。使用Class类的getMethods()方法可以获取这个类所包含的所有方法,使用getConstructors()方法可以获取这个类的所有构造函数。前文也提到过,使用Class.forName()可以用来动态的加载类。它的生成结果在编译的时候是不可知的,因此所有的方法特征信息和签名都是在运行时被提取出来的。

七、动态代理

    代理是常用的设计模式之一,它的作用是在基本的对象操作之外,增加一些其它额外的操作。比如我想使用一个对象,同事想了解这个对象的运行过程,那么这个运行过程的监控显然就不能放在基本的对象代码中。有点类似前面文章中提到的组合类的思想,新建一个代理类,代理类引用要使用的对象,然后增加一些新的功能。刚好前几天跟公司一个同事面了一个新人,同事问道代理模式之后自己做了一些阐述,可以把代理模式类比成是中介,中介的目的是卖东西的基础上赚钱,卖东西就是基本操作,赚钱就是中介也就是代理做的额外的操作。下面一个示例展示简单的代理模式:

先定义一个接口:

1
2
3
4
5
6
package com.chenxyt.java.practice;

public interface Interface {
void doSomeThing();
void somethingElse(String arg);
}

然后是这个接口的实现类,也就是前边所说的真正要操作的运行的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.practice;

public class RealObject implements Interface {

@Override
public void doSomeThing() {
// TODO Auto-generated method stub
System.out.println("RealObject DoSomeThing");
}

@Override
public void somethingElse(String arg) {
// TODO Auto-generated method stub
System.out.println("RealObject somethingElse");
}

}

    然后定义一个代理类,代理类实现了Interface接口,同时通过传参的形式传入了前边的实现类,在完成实现类功能的基础上,做了自己的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.chenxyt.java.practice;

public class SimpleProxy implements Interface{

private Interface proxied;

public SimpleProxy(Interface proxied) {
// TODO Auto-generated constructor stub
this.proxied = proxied;
}
@Override
public void doSomeThing() {
// TODO Auto-generated method stub
System.out.println("Proxy DoSomething");
proxied.doSomeThing();

}
@Override
public void somethingElse(String arg) {
// TODO Auto-generated method stub
System.out.println("Proxy somethingElse");
proxied.somethingElse(arg);
}

}

    最后是Main方法,因为consumer方法传参是Interface接口,所以任何实现了它接口的实体类都可以当做参数。这里演示了使用基本的实体类和使用代理的区别,代理在完成普通实体类的功能基础上打印了自己的操作内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;

public class SimpleProxyDemo {
public static void consumer(Interface iface){
iface.doSomeThing();
iface.somethingElse("bobo");
}
public static void main(String[] args) {
consumer(new RealObject());
System.out.println("==============");
consumer(new SimpleProxy(new RealObject()));
}
}

png5

    在任何时刻你若想实现一些与“实际”对象分离的额外操作,或者你希望很容易能对这部分操作做出修改,那么使用代理模式无疑是最为方便的。

    Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态的创建代理,并且动态的处理对所代理方法的调用。动态代理所做的所有操作都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型,并确定相应的对策。下面用动态代理重写上边的示例。

    Java中要实现动态代理类,必须要继承InvocationHandle这个类,这个类内部嵌入的对象是要被实现的真正的对象,同样使用构造方法传入,这个类唯一的一个方法invoke,它有三个参数,第一个参数是生成的动态代理类。这里我个人理解,既然动态代理是动态的创建代理,那么这个参数固然是所创建的动态代理,第二个参数是传入的对象执行的方法,第三个参数是传入的参数。最后将请求通过Method.invoke()方法,传入必要的参数,执行代理对象的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.chenxyt.java.practice;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicProxyHandler implements InvocationHandler{

private Object proxied;

public DynamicProxyHandler(Object proxied) {
// TODO Auto-generated constructor stub
this.proxied = proxied;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("**** Proxy:" + proxy.getClass() + ",method:" + method + ",args " + args );
if(args!=null){
for(Object arg:args){
System.out.println(" " + arg);
}
}
// TODO Auto-generated method stub
return method.invoke(proxied, args);
}
}

客户端类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.practice;

import java.lang.reflect.Proxy;

public class SimpleDynamicProxyDemo {
public static void consumer(Interface iface){
iface.doSomeThing();
iface.somethingElse("DIDI");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
System.out.println("=========");
Interface proxy = (Interface)Proxy.newProxyInstance(Interface.class.getClassLoader(),new Class[]{Interface.class},new DynamicProxyHandler(real));
consumer(proxy);
}
}

    客户端类使用Proxy.newProxyInstance()方法创建了一个代理对象。第一个参数是一个加载器,这里用的是系统加载器,所以使用其它类的加载器结果也是一样的,第二个参数是代理对象要实现的接口,第三个参数是Handler对象,表示我这个代理对象在调用方法时,会映射关联到哪个Hnadler上去,这里是关联到了我们定义的DynamicProxyHandler上,然后转由它的invoke方法执行对象方法。

运行结果:

png6

    我们可以在代理方法invoke()中对诸如方法名、参数进行过滤,只执行指定的方法。

    最近项目中用到了AOP做日志记录的功能,Spring的AOP核心思想就是动态代理,现在更加理解了一些,简单表述一下就是在切入点处执行一写其它操作,如我这里是记录日志,然后执行正常的业务方法。记录日志就是脱离在实际业务之外的一些操作。

八、空对象

    这里感觉不是很常用,大概理解了一下,就是当一个对象为null的时候,任何对这个对象的操作都会引发异常。为了避免这种情况,当一个对象为null的时候,我们定义一个空对象赋值给它。何谓空对象呢?就是一个不存在实际意义但是不会引发异常的对象。文中具体代码就不写了。

九、接口与类型信息

    接口或者是面向接口编程的重要目的是实现隔离,也就是解耦。但是通过类型信息,这种耦合还是会传播出去。也就是接口对解耦来说,并不是绝对的。如下示例:

定义一个接口A,它有唯一一个方法f

1
2
3
4
5
package com.chenxyt.java.practice;

public interface A{
public void f();
}

接下来是接口的实现B,除了A中的方法,还有自己的方法g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.chenxyt.java.practice;

public class B implements A{

@Override
public void f() {
// TODO Auto-generated method stub
System.out.println("method f");
}
public void g(){
System.out.println("method g");
}

}

Main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;

public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
System.out.println("a.getClass: " + a.getClass().getName());
if(a instanceof B){
B b = (B)a;
b.g();
}
}
}

运行结果:

png7

    这里通过使用RTTI,发现a是被当做B类型实现的,因此通过将其转型成B,可以调用了B中新加的方法。

    上面这种操作,完全合理并且可接受,但是提高了代码的耦合度,实际上并不允许这样做。解决办法是使用包访问权限来加以控制。

新定义一个实现类实现接口A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.chenxyt.java.practice;

class C implements A{
@Override
public void f() {
// TODO Auto-generated method stub
System.out.println("C.f");
}
public void g(){
System.out.println("C.g");
}
void u(){
System.out.println("C.u");
}
protected void v(){
System.out.println("C.v");
}
private void w(){
System.out.println("C.w");
}
}
public class HidenC{
public static A makeA(){
return new C();
}
}

在另一个包中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.chenxyt.java.test;


import java.lang.reflect.Method;

import com.chenxyt.java.practice.A;
import com.chenxyt.java.practice.HidenC;

public class HiddenImplementation {
public static void main(String[] args) throws Exception {
A a = HidenC.makeA();
a.f();
System.out.println("a.getClass: " + a.getClass().getName());
//编译错误 受包访问权限控制
/* if(a instanceof C){
C c = (C)a;
c.g();
}*/
callHiddenMethod(a,"g");
callHiddenMethod(a,"v");
callHiddenMethod(a,"u");
callHiddenMethod(a,"w");

}
static void callHiddenMethod(Object a,String methodName) throws Exception{
Method g = a.getClass().getDeclaredMethod(methodName);
g.setAccessible(true);
g.invoke(a);
}
}

运行结果:

png8

    可以看到由于包访问权限的限制,在另一个包中获取不到了C新定义的方法。但是如果知道方法名,依然可以通过反射调用所有的方法。当然如果通过只发布编译之后的代码或许可以阻止这种情况发生,但是实际结果并不是这样。如下:

png9

    使用javap -private 可以看到包括private权限的方法在内的所有方法。因此任何人都可以获取到你这个类中方法名,然后通过反射调用他们。

接下来将接口实现为私有内部类看一下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.chenxyt.java.test;
import com.chenxyt.java.practice.A;
class InnerA{
private static class C implements A{
@Override
public void f() {
// TODO Auto-generated method stub
System.out.println("C.f");
}
public void g(){
System.out.println("C.g");
}
void u(){
System.out.println("C.u");
}
protected void v(){
System.out.println("C.v");
}
private void w(){
System.out.println("C.w");
}
}
public static A makeA(){
return new C();
}
}
public class InnerImplemention {
public static void main(String[] args) throws Exception {
A a = InnerA.makeA();
a.f();
System.out.println("a.getClass " + a.getClass().getName());
HiddenImplementation.callHiddenMethod(a,"g");
HiddenImplementation.callHiddenMethod(a,"u");
HiddenImplementation.callHiddenMethod(a,"v");
HiddenImplementation.callHiddenMethod(a,"w");
}
}

运行结果:

png10

    可见使用内部类还是不能躲避反射的查找。

接下来试一下匿名类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.chenxyt.java.test;

import com.chenxyt.java.practice.A;

class AnoymousA{
public static A makeA(){
return new A(){
@Override
public void f() {
// TODO Auto-generated method stub
System.out.println("C.f");
}
public void g(){
System.out.println("C.g");
}
void u(){
System.out.println("C.u");
}
protected void v(){
System.out.println("C.v");
}
private void w(){
System.out.println("C.w");
}
};
}
}
public class AnoymousImplemention {
public static void main(String[] args) throws Exception {
A a = AnoymousA.makeA();
a.f();
System.out.println("a.getClass" + a.getClass().getName());
HiddenImplementation.callHiddenMethod(a,"g");
HiddenImplementation.callHiddenMethod(a,"u");
HiddenImplementation.callHiddenMethod(a,"v");
HiddenImplementation.callHiddenMethod(a,"w");
}
}

运行结果:

png11

    可见匿名类也被反射给找到了。即便是private域的方法也逃不过反射的到来。

    书中最后一段讲述,如果有人通过这种所谓的投机的方式获取内部私有的方法,那么他就应该接受这种方法改变之后带来的后果。

十、总结

    反射这一章节,看的前后拖的时间比较长了,基本理解了反射的概念,比如在运行时获取类型信息,通过class对象可以获得这个类的全部信息啊一些,因为文中涉及到一些设计模式,所以又补充了一下设计模式的知识。

【Java编程思想】十四:字符串

发表于 2018-11-19 | 分类于 学习笔记 | 阅读次数:

    大量程序表明,字符串操作是程序设计中的最基本操作。

一、不可变String

    String对象是不可变的,每一个看似修改了String值的方法,实际都是产生了一个新的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
public class Immutable {
public static String upCase(String s){
return s.toUpperCase();
}
public static void main(String[] args) {
String q = "howdy";
System.out.println(q);
String qq = upCase(q);
System.out.println(qq);
System.out.println(q);
}
}

运行结果:

png1

    当把q传给upCase方法时,实际上传递的是引用的一个拷贝。其实每当String对象作为参数传递时,传递的都是一个拷贝。再看upCase方法,只有当该方法运行时,局部引用s才存在,一旦该方法结束,引用s就消失了。该方法的返回值,实际上是最终值的引用,也就是upCase返回的引用已经指向了一个新的对象,而原本的q并没有发生任何变化。比如如下的方法,我们并不希望在经过一个操作之后,改变原有的对象。

1
2
String s = "abcde";
String x = Immutable.upCase(s);

    因为方法的参数是用来传递信息的,而不是用来改变原有对象本身的。

二、重载“+”与StringBuilder

    String对象是不可变的,所以指向它的任何引用都不会改变该对象的值。不可变性会对效率带来一个问题,为String对象重载“+”操作符就是一个例子,重载的意思是一个操作符应用与特定的类时被赋有特殊的意义。(用于String的“+”和”+=”是Java中仅有的两个重载过的操作符,Java不允许程序员自己重载操作符)
操作符“+”可以用来连接两个String

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
public class Concatetion {
public static void main(String[] args) {
String mango = "mango";
String s = "abc" + mango + "def" + 42;
System.out.println(s);
}
}

运行结果:

png2

    上述代码的运行过程可能是这样的:String可能有一个append方法,然后它会生成一个新的String对象用来连接abc和mango,然后该对象再与def相连生成新的对象,依次类推。这样做的话会产生很多中间垃圾需要清理因此它效率极低。
我们使用JDK自带的反编译工具查看上述代码如何工作:

png3

    我们代码中并没有使用StringBuilder类,然而编译器却自动的引入了StringBuilder类,从编译后的代码可以看出,字符串连接工作主要的操作是编译器创建一个StringBuilder对象,然后调用该对象的append()方法将所有的要连接的字符串连接到后边,最后调用toString()方法转换成String对象存给s。
    因此在编写一个类似toString()的方法时,如果字符串较短时,我们可以使用普通的拼接方式,当字符串操作较为复杂的时候,我们在代码中直接创建一个StringBuilder对象进行操作效率会更加优异。

三、无意识的递归

    我们希望使用toString()方法打印出对象的内存地址,那么我们可能会考虑使用this关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.chenxyt.java.practice;
import java.util.ArrayList;
import java.util.List;
public class InfiniteRecursion {
public String toString(){
return "InfiniteRecursion address" + this;
}
public static void main(String[] args) {
List<InfiniteRecursion> v = new ArrayList<InfiniteRecursion>();
for(int i = 0;i<10;i++){
v.add(new InfiniteRecursion());
}
System.out.println(v);
}
}

运行结果:

png4

这里当运行

1
return "InfiniteRecursion address" + this;

时发生了类型转换,编译器发现“+”后边不是String类型,会试图转换成String类型,转换的方式就是调用toString方法,因此会递归调用,此处如果想正确打印地址,那么需要使用其基类Object的toString()方法。

四、String上的操作

    所有的类都是Class类的对象,使用Class类的getMethods()方法可以返回该类的所有方法:

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
import java.lang.reflect.Method;
public class StringMethods {
public static void main(String[] args) {
Class<String> c = String.class;
Method[] methods = c.getMethods();
for(Method method :methods){
System.out.println(method);
}
}
}

    运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public int java.lang.String.hashCode()
//比较两个字符串 重载了Object的方法 当两个字符串值相同、类型相同则认为相同 忽略了引用
public boolean java.lang.String.equals(java.lang.Object)
public java.lang.String java.lang.String.toString()
//获取指定索引下标位置上的字符
public char java.lang.String.charAt(int)
public int java.lang.String.codePointAt(int)
public int java.lang.String.codePointBefore(int)
public int java.lang.String.codePointCount(int,int)
public int java.lang.String.compareTo(java.lang.Object)
//按词典顺序比较两个字符串 大小写并不等价
public int java.lang.String.compareTo(java.lang.String)
public int java.lang.String.compareToIgnoreCase(java.lang.String)
//字符串连接 返回一个新的String为指定String连接参数
public java.lang.String java.lang.String.concat(java.lang.String)
//查找字符串中是否包含指定字符 存在返回true
public boolean java.lang.String.contains(java.lang.CharSequence)
//比较两个字符串
public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
public boolean java.lang.String.contentEquals(java.lang.CharSequence)
public static java.lang.String java.lang.String.copyValueOf(char[])
public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
public boolean java.lang.String.endsWith(java.lang.String)
//比较字符串是否相同 忽略大小写
public boolean java.lang.String.equalsIgnoreCase(java.lang.String)
public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[])
public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[])
public void java.lang.String.getBytes(int,int,byte[],int)
public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException
public byte[] java.lang.String.getBytes(java.nio.charset.Charset)
public byte[] java.lang.String.getBytes()
// s.getChars(1,2,char,3) 赋值s串中下标为1-2的到char中 char中起始位置为3
public void java.lang.String.getChars(int,int,char[],int)
public int java.lang.String.indexOf(int,int)
public int java.lang.String.indexOf(java.lang.String,int)
//是否包含该字符 包含返回下标 否则返回-1
public int java.lang.String.indexOf(java.lang.String)
public int java.lang.String.indexOf(int)
public native java.lang.String java.lang.String.intern()
public boolean java.lang.String.isEmpty()
public int java.lang.String.lastIndexOf(int,int)
public int java.lang.String.lastIndexOf(java.lang.String)
public int java.lang.String.lastIndexOf(java.lang.String,int)
public int java.lang.String.lastIndexOf(int)
//String 中字符的个数
public int java.lang.String.length()
public boolean java.lang.String.matches(java.lang.String)
public int java.lang.String.offsetByCodePoints(int,int)
public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int)
public boolean java.lang.String.regionMatches(int,java.lang.String,int,int)
//把字符串中的第一个参数字符替换成第二个
public java.lang.String java.lang.String.replace(char,char)
public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence)
public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String)
public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String,int)
public java.lang.String[] java.lang.String.split(java.lang.String)
public boolean java.lang.String.startsWith(java.lang.String,int)
//字符串起始串
public boolean java.lang.String.startsWith(java.lang.String)
public java.lang.CharSequence java.lang.String.subSequence(int,int)
//字符串截断
public java.lang.String java.lang.String.substring(int)
public java.lang.String java.lang.String.substring(int,int)
//返回一个字符数组,该字符数组包含字符串的所有字符
public char[] java.lang.String.toCharArray()
//字符串字符转换成小写
public java.lang.String java.lang.String.toLowerCase()
public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
//字符串字符转换成大写
public java.lang.String java.lang.String.toUpperCase()
public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
//删除String两端的空白字符
public java.lang.String java.lang.String.trim()
public static java.lang.String java.lang.String.valueOf(char)
public static java.lang.String java.lang.String.valueOf(float)
public static java.lang.String java.lang.String.valueOf(int)
public static java.lang.String java.lang.String.valueOf(long)
public static java.lang.String java.lang.String.valueOf(double)
public static java.lang.String java.lang.String.valueOf(java.lang.Object)
public static java.lang.String java.lang.String.valueOf(char[])
public static java.lang.String java.lang.String.valueOf(char[],int,int)
public static java.lang.String java.lang.String.valueOf(boolean)
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException

注释了一些常用的方法,其它的方法可自行查阅。

五、格式化输出

    JavaSE5提供了格式化输出功能,这一功能使得控制输出的功能变得更加简单,同时也给开发者带来了更加强大的代码输出控制能力。JavaSE5引入的format方法可以用于PrintStream或PrintWriter对象,其中也包括System.out对象。format()方法模仿在C语言的printf()如下简单示例:

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class SimpleFormat {
public static void main(String[] args) {
int x = 5;
double y = 5.333221;
System.out.println("Row1: [" + x + " " + y + "]");
System.out.format("Row1: [%d %f]\n",x,y);
System.out.printf("Row1: [%d %f]\n",x,y);
}
}

运行结果:

png5

可以看到,format()与printf()相同,只需要加上对应的格式化字符即可。

    Java中所有新的格式化功能都由java.util.Formatter类处理,可以将其看做是一个翻译器,它将你的格式化字符串和数据翻译成想要的结果。当你创建了一个Formatter对象的时候,需要向编译器传递一些信息,告诉他最终的结果将向哪里输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;

import java.io.PrintStream;
import java.util.Formatter;
public class Tutle {
private String name;
private Formatter f;
public Tutle(String name,Formatter f){
this.f = f;
this.name = name;
}
public void move(int x,int y){
f.format("%s The Tutle is at (%d,%d)\n",name,x,y);
}
public static void main(String[] args) {
Tutle tommy = new Tutle("Tommy",new Formatter(System.out));
Tutle terry = new Tutle("Terry",new Formatter(System.err));
tommy.move(0, 4);
terry.move(4, 8);
}
}

运行结果:

png6

如上我们指定了格式化输出的结果分别打印到了System.out和System.err上

    有的时候我们希望做一些更精致的格式化信息,比如控制空格与对齐,最常见的是控制域的最小尺寸,这可以通过指定width实现,Formatter对象通过在必要时添加空格,来确保一个域至少达到某个长度。默认情况下域是右对齐的,不过也可以通过“-”指定数据的对齐方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.chenxyt.java.practice;
import java.util.Formatter;
public class Reciept {
private double total = 0;
private Formatter f = new Formatter(System.out);
public void printTitle(){
f.format("%-15s %5s %10s\n","Item","Qty","Price");
f.format("%-15s %5s %10s\n","----","---","-----");
}
public void print(String name,int qty,double price){
f.format("%-15.15s %5d %10.2f\n",name,qty,price);
total+=price;
}
public void printTotal(){
f.format("%-15.15s %5s %10.2f\n","Tax","",total*0.06);
f.format("%-15.15s %5s %10s\n","","","------");
f.format("%-15.15s %5s %10.2f\n","Total","",total*1.06);
}
public static void main(String[] args) {
Reciept receipt = new Reciept();
receipt.printTitle();
receipt.print("Jack's Magic Beans",4,4.25);
receipt.print("Princess Beans",3,5.1);
receipt.print("Thres Bears Porridge",1,14.25);
receipt.printTotal();
}
}

运行结果:

png7

可以看到Formatter类提供了很强大的格式化支持

Formatter类也有很多类型转换,常用的类型转换如下

d:整数型(十进制)e:浮点数(科学计数)c:Unicode字符 x:整数(十六进制)b:Boolean值 h:散列码(十六进制)s:String %:字符“%”f:浮点数(十进制),针对不同的数据类型,有些转换是无效的,如果强制使用则会发生异常。

六、正则表达式

    正则表达式就是以某种方式来描述字符串,因此你可以说“如果一个字符串含有某些东西,那么它就是我需要的东西”。\d表示一位数字,Java中对于\反斜线有不同的处理,在其它语言中,\表示我想插入一个普通的反斜线,而在Java中\表示我将要插入一个正则表达式反斜线,所以它后边的值应该具有特殊意义。例如你想表示一个数字,那么就是\d,如果想插入一个普通的反斜线,则需要使用\\,不过制表之类的应该使用单反斜线,\d\n,使用?表示可能存在,使用+表示一个或多个+之前的表达式。比如我们要判断可能有一个负号后边跟着一个或多个数字则使用:

-?\d+

应用正则的最简单方式是使用String类提供的方法:

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;
public class IntegerMatch {
public static void main(String[] args) {
System.out.println("-1234".matches("-?\\d+"));
System.out.println("5678".matches("-?\\d+"));
System.out.println("+911".matches("-?\\d+"));
System.out.println("+911".matches("(-|\\+)?\\d+"));
}
}

运行结果:

png8

    前两个满足正则表达式的结果,第三个“+“作为一个独立的符号,所以与”可能有个负号开头的一个或多个整数“不匹配,应该使用”可能有一个正号或者符号开头的一个或多个整数“,括号有着分组的作用,|表示或。

    String类还带有一个非常有用的正则表达式方法split(),功能是将字符串在正则表达式匹配的地方分割开:

1
2
3
package com.chenxyt.java.practice; import java.util.Arrays; public class Splitting { public static String knights = "Then。when you have found the shrubbery.you must" + "cut down the mightiest tree in the forest。。。" + "with... a herring"; public static void split(String regex){ System.out.println(Arrays.toString(knights.split(regex))); } public static void main(String[] args) { split(" "); split("\\W+");
split("\\w+");
split("n\\W+"); } }

运行结果:

png9

    第一个是使用空格拆分,\W表示非单词字符,\w表示单词字符,第三个表示字母n以及后边的一个或多个非单词字符。可见与正则表达式相同的内容都消失了。

    String类自带的最后一个正则表达式方法为替换,你可以替换正则表达式第一个匹配的子串,也可以替换所有匹配的地方。

1
2
3
4
5
6
7
public class Replacing {  
    static String s = Splitting.knights;  
    public static void main(String[] args) {  
        System.out.println(s.replaceFirst("f\\w+","located"));  
        System.out.println(s.replaceAll("shrubbery|tree|herring","banana"));  
    }  
}

运行结果:

png10

    第一个正则表示以字母f开头后面跟着一个或多个字母的,将第一个匹配的子串替换成“located”,第二个表达式是替换三个单词中的任意一个,只要存在则替换。

    我们也可以自己创建正则表达式,java.util.regex.Pattern类提供了完整的正则表达式列表,先列举一些常用的:

png11

    以下是一些字符类创建的典型方式,以及一些预定义类。

png12

作为演示,下面的每一个正则表达式都可以成功的匹配“Rudolph“

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
public class Rudolph {
public static void main(String[] args) {
for(String parten:new String[]{"Rudolph","[rR]udolph","[rR][aeiou][a-z]ol.*","R.*"}){
System.out.println("Rudolph".matches(parten));
}
}
}

运行结果:

png13

    我们的任务是在能够完成任务的情况下编写简单的易于理解的正则表达式,而不是编写难以理解的正则表达式。
我们在使用正则表达式的时候很容易混淆,因为他是一种在Java之上的新语言,CharSequence,该接口从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义。

1
2
3
4
5
6
7
package com.chenxyt.java.practice;
public interface CharSequence {
char charAt(int i );
int length();
String subSequence(int start,int end);
String toString();
}

因此,这些类都实现了该接口,多数正则表达式操作都接受CharSequence类型的参数。

    String类作为正则表达式的匹配使用功能有限,因此我们可以使用更为强大的正则表达式对象。只需要导入java.util.regex包,然后使用Pattern.compile()方法编译正则表达式。它会根据传入的String类型的正则表达式生成一个Pattern对象,接下来把我们想要检索的字符串传入Pattern.matcher()方法,该方法会生成一个Matcher对象,它有很多功能可以使用。
以下为演示示例,第一个控制台参数为要匹配的字符串,后边一个或多个参数都为正则表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.chenxyt.java.practice;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TestRegularExpression {
public static void main(String[] args) {
if(args.length<2){
System.out.println("Usage:\n java TestRegularExpression " + "characterSequence regularExpression+");
System.exit(0);
}
System.out.println("Input: \"" + args[0] + "\"");
for(String arg:args){
System.out.println("Regular expression: \"" + arg + "\"");
//编译正则表达式
Pattern p = Pattern.compile(arg);
//传入字符串进行匹配
Matcher m = p.matcher(args[0]);
while(m.find()){
System.out.println("Match \"" + m.group() + "\" at positions " + m.start() + "-" + (m.end() - 1));
}
}
}
}

运行结果:

png14

    Matcher.find()方法用来查找字符串中的多个匹配。start()方法返回匹配的起始位置的索引,end()方法匹配的终止位置的索引。

七、扫描输入

    目前来说,读取文本或者从标准输入读取数据一般的解决方法是读入一行文本,然后对其进行分词,然后使用Integer、Double的各种解析方法来解析数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.chenxyt.java.practice;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class SimpleRead {
public static BufferedReader input = new BufferedReader(new StringReader("Sir Robin of Camelot\n22 1.523222"));
public static void main(String[] args) {
try{
System.out.println("What's your name?");
String name = input.readLine();
System.out.println(name);
System.out.println("How old are you?What's your favorite double");
String numbers = input.readLine();
System.out.println(numbers);
String numArray[] = numbers.split(" ");
int age = Integer.parseInt(numArray[0]);
double favorite = Double.parseDouble(numArray[1]);
System.out.format("Hi %s.\n",name);
System.out.format("In 5 years you will be %d. \n",age+5);
System.out.format("My Favorite double is %f.",favorite/2);
}catch(IOException e){
System.err.println("I/O Exception");
}
}
}

运行结果:

png15

    以上代码中,使用了IO中的readLine()方法读取输入流中的一行,然后进行解析。终于在JavaSE5新增了Scanner类,它可以大大的减轻扫描输入的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.chenxyt.java.practice;
import java.util.Scanner;
public class BetterRead {
public static void main(String[] args) {
Scanner stdin = new Scanner(SimpleRead.input);
System.out.println("What's your name?");
String name = stdin.nextLine();
System.out.println(name);
System.out.println("How old are you?What's your favorite double");
System.out.println("(input:<age><double>)");
int age = stdin.nextInt();
double favorite = stdin.nextDouble();
System.out.println(age);
System.out.println(favorite);
System.out.format("Hi %s.\n",name);
System.out.format("In 5 years you will be %d. \n",age+5);
System.out.format("My Favorite double is %f.",favorite/2);
}
}

运行结果:

png16

    Scanner的构造器可以接受任意类型的输入对象,其普通的next方法将返回一个String,而所有的基本类型都有一个next方法,该方法的作用是,读取到下一个完整的指定类型的分词之后才返回。同时Scanner还有对应的hasNext方法,用来判断是否有所输入分词的类型。
Scanner默认的定界符为空白符,我们也可以自己使用正则表达式指定分隔符:

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
import java.util.Scanner;
public class ScannerDelimiter {
public static void main(String[] args) {
Scanner scanner = new Scanner("12,24 ,35,67");
scanner.useDelimiter("\\s*,\\s*");
while(scanner.hasNextInt()){
System.out.println(scanner.nextInt());
}
}
}

运行结果:

png17

    这个例子使用逗号以及任意的空白字符来实现字符串的分割。使用useDelimiter()方法设置定界符,也可以使用delimiter()方法返回现在所使用的定界符。

    Scanner除了可以扫描基本类型外,还可以扫描正则表达式,这在扫描复杂数据类型的时候非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.chenxyt.java.practice;

import java.util.Scanner;
import java.util.regex.MatchResult;

public class ThreadAnalyzer {
static String threatData = "58.27.82.161@02/10/2005\n" +
"204.45.234.40@02/11/2005\n" +
"58.27.82.161@02/11/2005\n" +
"58.27.82.161@02/11/2005\n" +
"58.27.82.161@02/11/2005\n" +
"[Next log section width different data format]";
public static void main(String[] args) {
Scanner scanner = new Scanner(threatData);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@" + "(\\d{2}/\\d{2}/\\d{4})";
while(scanner.hasNext(pattern)){
scanner.next(pattern);
MatchResult match = scanner.match();
String ip = match.group(1);
String date = match.group(2);
System.out.format("Thread on %s from %s\n",date,ip);

}
}

}

运行结果:

png18

    当next()方法配合指定正则表达式使用时,将找到下一个匹配该模式的输入部分,调用match()方法获得匹配的结果。有一点需要注意,它只针对下一个输入分词进行匹配,如果正则表达式含有定界符,那将永远不能成功。

八、StringTokenizer

    在正则表达式和Scanner引入之前,我们使用的是StringTokenizer来进行字符串匹配,下面演示这种方式并与其它方式进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.chenxyt.java.practice;

import java.util.Arrays;
import java.util.Scanner;
import java.util.StringTokenizer;

public class ReplacingStringTokenizer {
public static void main(String[] args) {
String input = "But I'm not dead yet!I feel happy!";
StringTokenizer stoke = new StringTokenizer(input);
while(stoke.hasMoreElements()){
System.out.print(stoke.nextToken() + " ");
}
System.out.println();
System.out.println(Arrays.toString(input.split(" ")));
Scanner scanner = new Scanner(input);
while(scanner.hasNext()){
System.out.print(scanner.next() + " ");
}

}
}

运行结果:

png19

    使用正则表达式或者Scanner我们可以使用更加复杂的模式匹配字符串,而使用StringTokenizer则很困难了,因此实际上StringTokenizer这个类基本上已经被废弃了。

九、总结

    String类型作为程序设计中最为常见的一种操作类型,它是一种不可变的操作类型。它内部重载了“+”操作符,使得可以使用”+”操作符完成字符串的拼接工作,拼接的原理是编译器为我们自动生成了StringBuilder类来完成。因此对于复杂的字符串拼接操作,我们自己使用StringBuilder效率会更高一些。String还有很多常用的方法,正则表达式为我们提供了字符串匹配的多种形式。

【Java编程思想】十三:通过异常处理错误

发表于 2018-11-16 | 分类于 学习笔记 | 阅读次数:

    错误的理想时机是在程序的编写过程中,但是一些业务逻辑错误,在编写过程中很难发现。这些问题就只能在程序运行的过程中解决。这就需要程序在运行过程中发生错误的时候能够准确的提供信息给某个接收者,以便可以正确的处理错误。

    Java中提供了一种非常优秀的处理错误的手段,被称为“异常处理”,下文讲述如何编写正确的异常处理程序,并将展示方法出现问题时,如何自定义异常。

一、概念

    C语言以及其它早期的编程语言都是采用一种约定俗称的模式,比如在发生错误的时候,返回一个错误标记,或者设置一个特殊的变量来记录错误。这种形式需要增加大量的判断以及额外的代码无疑使程序更加繁重不利于构建。

    解决的方法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。“异常”这个词有“我对此感到意外”的意思,当错误问题出现了,你可能不知道出现在哪里,或者出现了什么错误,也不知道怎么解决。那么就停下来,将这个问题提供给更高的环境,看看有没有正确的解决方案。使用异常的另一个好处是,它能够明显的降低代码的复杂程度,避免了大量的错误检查,只需要在一个特定的地方进行异常捕获,并且不需要做任何判断,异常捕获区能够捕获所有发生的错误。这种异常的处理方式与之前的错误处理方式相比,完全的将“正常做的事儿”与“出现问题怎么办”隔离开来。是代码的读写变得更加井井有条。

二、基本异常

    “异常情形”是指阻止当前方法或者作用域继续执行的问题,要把异常情形与普通问题区分开来,普通问题是指在当前的错误收集情况下,能够解决这个问题并继续执行正常的程序。而对于异常情形,这个程序就不能够正常的执行下去了。
    除法是一个简单的例子,除数有可能为0,所以先进行检查很有必要。但除数为0如果是一个意外的值,你也不清楚该如何处理,那么抛出异常就显得尤为重要了,而不是顺着原来的路走下去。
    当抛出异常之后有几件事儿会随之发生,同 Java中其它对象的创建相同,将使用new在堆上创建对象,然后当前程序的执行路径被终止了,因为发生了异常,不能继续执行下去,并且从当前执行的环境中弹出异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序。它的任务是使程序从错误的状态中恢复,要么换一种方式运行,要么继续运行下去。
    举一个抛出异常的简单例子,对于一个对象引用t,传递给你的时候可能没有被初始化,所以在使用这个对象引用调用执行方法之前,进行合理的判断是非常有必要的。可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误异常抛到更大的环境中去了。所以一个异常,看起来是这样的:

1
2
3
if(t==null){
throw new NullPointerException();
}

    上面代码中,我们判断当前对象引用是否没有进行初始化,如果没有进行初始化,那么就创建一个NullPointerException()对象,然后使用throw关键字,将该对象的引用抛出。

    异常使我们将每件事都当做一个事务考虑,我们还可以将异常看作是一种内建的恢复系统,因为我们在程序中拥有不同的恢复点,如果程序的某部分失败了,异常将“恢复”到程序的某个已知点上。异常最大的好处就是在发生错误的时候能够强制程序终止当前线路的运行。
    同其它对象的处理方式一样,我们使用new关键字在堆上创建异常对象,因此同样伴随着存储空间的分配和构造器的调用。所有标准异常器都有两个构造函数,一个是无参构造函数,另一个是可以传递一个字符串的构造函数,将相关信息放入到构造器中。此外,异常处理能够抛出任意类型的Throwable对象,它是异常类型的根类。通常对于不同类型的错误,要抛出对应的异常,错误信息可以保存在异常对象内部,或者使用异常的名称来暗示。

三、捕获异常

    我们可以在程序中专门定义一个方法块儿,用来尝试各种可能产生异常的方法,这个块的定义形式如下:

1
2
3
try{
//---
}

    在关键字try后包围的一部分代码块,称作try块,这样做比起之前说的要在每个会产生错误的地方进行判断要容易的多,并且代码的可读性大大增强,产生异常之后抛出的异常必须在某处得到处理,这个地点就是异常处理区,异常处理区紧跟着try块,使用catch关键字表示:

1
2
try{ //---}catch(Type1 arg1){ //---}catch(Type2 arg2){
//---

    每个catch语句看起来像是一个接收一个且仅接收一个的特殊参数类型的方法。可以在方法块内部处理这个参数,当然有的异常见名知意,因此可以不用处理参数,但并不可以省略。可以有多个catch块与try对应,用来捕获多种不同的异常。异常处理程序必须紧跟在try块之后,当异常被抛出之后,异常处理机制负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入catch块中执行,此时认为异常得到了处理。一旦catch子句结束,则认为处理程序的查找过程结束。只有匹配的catch字句才会执行,在try块内部不同的方法可能会产生相同的异常,而你只需要提供一个针对此类型的异常处理程序。异常的处理理论上有两种模型,一种是终止模型,表示程序发生异常之后,已经无法恢复到程序正常执行的顺序上,程序被迫发生终止。另一种模型是恢复模型,表示异常发生时,我们要做的是处理错误,而不是抛出异常。目前终止模型已经基本取代了恢复模型,虽然这种恢复模型看起来很好,但是实际上使用起来并不容易。

四、创建自定义异常

    Java中虽然提供了很多默认的异常类型,但是要想完全覆盖会发生的异常情况显然是不现实的,因此我们可以自定义异常来表示我们预期可能会出现的异常。自定义的形式也非常简单,只需要继承一个相似的异常类即可。建立一个新的异常类最贱的方法就是让编译器为你产生默认构造器,所以这几乎不需要多少代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.chenxyt.java.practice;  
class SimpleException extends Exception{}  
public class InheritingException {  
    public void f() throws SimpleException{  
        System.out.println("Throw SimpleException from f");  
        throw new SimpleException();  
    }  
    public static void main(String[] args) {  
        InheritingException ite = new InheritingException();  
        try{  
            ite.f();  
        }catch(SimpleException e){  
            System.out.println("Caught it!");  
        }  
    } 
}

运行结果:

png1

    当然我们还可以创建一个带有参数的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.chenxyt.java.practice;

class MyException extends Exception{
public MyException(){

}
public MyException(String msg){
super(msg);
}
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println("Throwing myException from f()");
throw new MyException();
}
public static void g() throws MyException{
System.out.println("Throwing myException from g()");
throw new MyException("Originated in g()");
}
public static void main(String[] args) {
try{
f();
}catch(MyException e){
e.printStackTrace();
}
try{
g();
}catch(MyException e){
e.printStackTrace(System.out);
}
}
}

运行结果:

png2

    在带参数的异常构造器中,显示的调用了其基类的带参构造方法。在catch块的异常处理程序中,调用了在Throwable类(所有异常的基类)声明的printStackTrace方法,该方法如果不带参数则将异常信息也就是从方法调用处到异常产生出的所有方法调用信息打印到标准错误流上,如果带了标准输出流参数,则打印在标准输出流上。区别可以从控制台错误信息的眼神看出。标准错误流更加的引人注目。

五、异常说明

    Java提供了相应的语法(并强制使用这个语法),使你可以使用礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后,使用throws关键字,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像是这样:

1
void f() throws TooBig,TooSmall,DivZero{};

    代码必须与异常说明一致,比如上边的方法,如果我们在main()中调用,编译器会提示该方法会产生异常,要么使用try catch进行处理,要么使用throws关键字抛出这种异常。

六、捕获所有异常

     我们可以使用所有异常类型的基类Exception来捕获所有的异常,即不管发生什么类型的异常,都能被捕获到。

1
2
3
catch(Exception e){
//---
}

    Exception是与编程相关的所有异常的基类(还有其它的基类),所以它不会具有太多的信息。不过可以调用它从其基类Throwable继承的方法

String getMessage() 获取详细信息
String getLocalizedMessage()获取用本地语言表示的详细信息
String toString()返回对Throwable的简单描述
void printStackTrace()打印Throwable的调用栈轨迹 输出到标准错误流
void printStackTrace(PrintStream)打印Throwable的调用栈轨迹 输出到可选择的流
void printStackTrace(PrintStream)打印Throwable的调用栈轨迹 输出到可选择的流
调用栈显示了“把你带到异常抛出地点”的方法调用序列。
    Throwable fillInStackTrace()用于在Throwable对象内部,记录栈帧的当前状态,这在程序重新抛出错误或者异常时很有必要。
    此外,也可以使用Throwable从其基类Object继承的方法,getClass()它将返回一个表示此类型的对象,然后可以使用getName()方法查询这个Class对象包含包信息的名称,也可以使用getSimpleName()返回只有类名的名称。
下面展示Exception类型方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.practice;
public class ExceptionMethods {
public static void main(String[] args) {
try{
throw new Exception("My Exception");
}catch(Exception e){
System.out.println("Caught Exception");
System.out.println("getMessage():" + e.getMessage());
System.out.println("getLocalizedMessage():" + e.getLocalizedMessage());
System.out.println("toString()" + e.toString());
System.out.println("printStackTrace():--==");
e.printStackTrace();
System.out.println("printStackTrace(System.out):--==");
e.printStackTrace(System.out);
}
}
}

运行结果:

png3

    printStackTrace()方法所提供的信息可以使用getStackTrace()方法获取到,这个方法返回一个由栈轨迹元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0为栈顶元素,表示调用序列最后的一个方法,也就是Throwable创建和抛出的地方,数组中最后一个元素为方法调用序列中的第一个调用方法。下面程序简单演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.chenxyt.java.practice;
public class WhoCalled {
static void f(){
try{
throw new Exception();
}catch(Exception e){
for(StackTraceElement ste : e.getStackTrace()){
System.out.println(ste.getMethodName());
}
}
}
static void g(){
f();
}
static void h(){
g();
}
public static void main(String[] args) {
f();
System.out.println("------------");
g();
System.out.println("------------");
h();
}
}

运行结果:

png4

可以看到第一个打印的是异常抛出的方法,最后打印的是main()函数方法。

    有时候我们希望把刚捕获的异常抛出,如下:

1
2
3
4
catch(Exception e){
System.out.println("caught an exception");
throw e;
}

    重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块后边的catch块将被忽略。此外,异常对象的所有信息都会被保持,所以上一级环境的异常捕获程序可以获得 这个异常的所有信息。如果只是单纯的把这个异常抛出,那么printStackTrace()显示的将是原来这个异常的栈调用信息。如果想更新这个序列信息,可以使用fillInStackTrace()方法,这将返回一个Throwable对象,它是把当前调用栈的信息,填入到原来那个异常对象而建立的。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.chenxyt.java.practice;
public class ReThrowing {
public static void f() throws Exception{
System.out.println("the exception in f()");
throw new Exception("exception from f()");
}
public static void g() throws Exception{
try{
f();
}catch(Exception e){
System.out.println("inside g e.printStackTrace");
e.printStackTrace();
throw e;
}
}
public static void h() throws Exception{
try{
f();
}catch(Exception e){
System.out.println("inside h e.printStackTrace");
e.printStackTrace();
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args) {
try{
g();
}catch(Exception e){
System.out.println("main:printStackTrace");
e.printStackTrace();
}
try{
h();
}catch(Exception e){
System.out.println("main:printStackTrace");
e.printStackTrace();
}
}
}

运行结果:

png5

    可以看出当使用了fillInStackTrace()方法之后,调用栈中的方法栈信息发生了变化。当然如果在捕获了第一种异常之后抛出了另一种异常,那么调用栈的信息也会发生变化,类似使用了fillInStackTrace()方法。但是这样做的话,原始的异常信息就消失了,有时候我们希望在抛出新的异常的时候能保留原来的异常信息,这被称作是异常链。在JDK1.4之前,程序员需要自己编写代码保存原来的异常信息,现在所有的Throwable子类都可以在构造函数中传入一个cause参数,这个参数就用来保存原始异常。这样就可以在当前位置创建并抛出新的异常,也可以通过这个异常链追踪到原始异常。

    现在在所有Throwable的子类中,只有三种基本异常类型提供了带有cause参数的构造器,分别是Error、Exception、RuntimeException,对于其他类型的异常链接,应该使用initCause()方法,而不是构造器。

七、Java标准异常

    Throwable这个类表示任何可以被作为异常抛出的类,Error表示编译时的系统错误,一般不需要我们关心。Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时故障都可以抛出此类异常。所以Java程序员通常需要关心此类的异常信息,同时Java异常要求可以见名知意。Java中还有一种Runtime异常,它表示程序编程错误,无法由程序员自己控制,这种异常不需要我们自己主动捕获并抛出,程序会自动抛出此类异常,如前文提到的NullPointerException。

八、使用finally进行清理

    对于一些代码,可能希望无论是否抛出异常,这些代码都必须要执行。这通常包括内存回收之外的一些操作,因为内存回收是由垃圾回收器完成。为了达到这个效果,可以在catch块之后加上finally块。在finally块中处理这些代码。完整的异常处理如下:

1
2
3
4
5
6
7
8
9
try{
//---
}catch(Exception e1){
//---
}catch(Exception e2){
//---
}finally{
//---
}

    下面的示例用来证明finally字句不管异常是否抛出都能被执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;
public class FinallyWorks {
static int count = 0;
public static void main(String[] args) {
while(true){
try{
if(count++ == 0){
throw new Exception();
}
System.out.println("No Exception");
}catch(Exception e){
System.out.println("Exception");
}finally{
System.out.println("In finally clause");
if(count == 2){
break;
}
}
}
}
}

    可以看到不管异常是否被抛出,finally字句都执行了。这个程序告诉我们,当程序发生了异常之后不能正确的回到原来的执行顺序上,我们可以在finally块中处理一些必要做的事情。比如打开文件的操作,当文件操作发生异常的时候,我们要确保文件被正确的关闭。

九、异常的限制

    当覆盖一个方法的时候,我们只能抛出在其基类方法异常说明列出的那些异常。这个限制的目的是符合面向对象的思想的,意味着其基类方法应用到派生类对象的时候,也能正常使用。异常也不例外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.chenxyt.java.practice;
class BaseballException extends Exception{

}
class Foul extends BaseballException{

}
class Strike extends BaseballException{

}
abstract class Inning{
public Inning() throws BaseballException{

}
public void event() throws BaseballException{

}
public abstract void atBat() throws Strike,Foul;
public void walk(){

}
}
class StormException extends Exception{

}
class RainedOut extends StormException{

}
class PopFoul extends Foul{

}
interface Storm{
public void events() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StormyInning extends Inning implements Storm{

public StormyInning() throws BaseballException {
super();
// TODO Auto-generated constructor stub
}

@Override
public void rainHard() throws RainedOut {
// TODO Auto-generated method stub

}

@Override
public void atBat() throws Strike, Foul {
// TODO Auto-generated method stub

}

@Override
public void events() throws RainedOut {
// TODO Auto-generated method stub

}
//抽象类中的该方法没有抛出任何异常 所以覆盖重写的时候 也不能抛出异常
// public void walk() throws RainedOut{
//
// }
//抽象类中的该方法抛出了异常 重写的时候可以选择不抛出异常
// public void event(){
//
// }
//抽象类中的该方法抛出了BaseballException 所以可以抛出其异常的子异常
public void event() throws Foul{

}
}

    如注释上所述,当继承或者实现一个类的时候,这个子类只能抛出基类中异常列表中的异常,或者其子异常,或者不抛出异常,但是不能抛出其它异常,或者抛出异常的基类异常。

十、构造器

    “如果异常发生了,所有东西能被正确清理吗?”大多数情况都是非常安全的,可以使用finally进行清理。但是当使用构造函数的时候,就有些问题了。比如使用构造函数构造文件操作,因为finally语句不管构造成功或者失败都会执行。假如构造失败了,如果文件没有被打开,那么这时候如果执行finally关闭文件操作就是不正确的。还有,如果文件构造成功了,因为文件需要在后边使用,所以在finally中关闭文件显然是不合理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.chenxyt.java.practice;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class InputFile {
private BufferedReader in;
public InputFile(String name) throws Exception{
try{
in = new BufferedReader(new FileReader(name));
}catch(FileNotFoundException e1){
//没有找到文件也就是文件没有打开 所以不需要清理
System.out.println("The file is not found");
throw e1;
}catch(Exception e2){
//所有其它的异常都发生在文件打开之后 因此需要进行清理工作
try{
in.close();
}catch(IOException e3){
//---
}
throw e2;
}finally{
//此处由于构造成功也会执行 所以不能进行文件关闭操作
}
}
}

    对于这种需要在构造器中创建的对象,合理的做法是使用嵌套try-catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.practice;
public class CleanUp {
public static void main(String[] args) {
try{
InputFile in = new InputFile("CleanUp.java");
try{
//
}catch(Exception e1){
//此处为文件操作异常
}finally{
//-- 此处清理操作关闭文件
}
}catch(Exception e2){
//--此处捕获构造异常
}
}
}

十一、异常匹配

    在抛出异常之后,异常处理系统会按照代码的编写顺序找到最近的异常处理程序并执行,此时便任务异常已经得到了处理,就不会继续进行下去。查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配,子类的对象也可以匹配到其基类的处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.chenxyt.java.practice;
class Annyoance extends Exception{

}
class Sneeze extends Annyoance{

}
public class Human {
public static void main(String[] args) {
try{
throw new Sneeze();
}catch(Sneeze s){
System.out.println("Catch Sneeze");
}catch(Annyoance a){
System.out.println("Catch Annyoance1");
}
try{
throw new Sneeze();
}catch(Annyoance a){
System.out.println("Catch Annyoance2");
}
}
}

运行结果:

png6

    编译器发现Annyoance是Sneeze的基类,所以编译的过程会提示警告。

十二、总结

    异常是Java编程中不可或缺的一部分,它将程序正常执行的路径与错误处理分开,减少了编码判断的冗余。使用try-catch的方式进行异常处理,同时我们也可以创建自己的异常,继承其它的异常类。一些RuntimeException无需我们自己捕捉,程序会自动捕捉。注意构造器内发生异常的情况,因为构造器往往是只是简单的构造了一个对象,对象还需要使用,所以在使用finally进行清理的时候需要注意这一点。因为finally域的代码块不管异常是否发生都会执行。处理异常的时候可以使用基类Throwable提供的方法,比如使用printStackTrace打印栈的调用顺序。

【Java编程思想】十二:持有对象

发表于 2018-11-15 | 分类于 学习笔记 | 阅读次数:

    在前面的学习过程中,我们使用的都是固定数量的且生命周期已知的对象。而在一些情况中,我们可能需要不确定数量不确切类型的对象,这种创建一个单一的对象显然是不行的了。Java提供了多种支持,比如数组,数组可以保存一组基本数据类型。但是数组的大小是固定的,在更特殊的编程条件下,固定长度显然是不友好的,所以Java类库提供了一套相当完整的容器类来解决这个问题。我们也称作是集合类。本章优先学习常用的集合以及用法,后续将会更加深入的讨论其它的集合。

一、泛型和类型安全的容器

    在JavaSE5泛型的概念出来之前,容器的一个主要问题就是编译器允许你在容器中插入任意类型的对象,这在一些情况下显然是不合理也是不可靠的。考虑这样一个情况,有一个存储Apple对象的容器,我们使用最基本最可靠的ArrayList,ArrayList你可以看成是一个可以自动扩充的数组。ArrayList需要使用索引就像数组下标一样,但是不需要使用方括号,它使用add()插入对象,使用get()获取对象,使用size()获取长度。

    在本例中Orange对象也会存在Apple容器中,正常情况下编译器会报出警告信息,此处使用@SuppressWarnings注解忽略没有类型检查的警告信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.chenxyt.java.practice;

import java.util.ArrayList;

class Apple{
private static long counter;
private final long id = counter++;
public long getid(){
return id;
}
}
class Orange{

}
public class AppleAndOrangesWithoutGeneric {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ArrayList al = new ArrayList();
for(int i=0;i<3;i++){
al.add(new Apple());
al.add(new Orange());
}
for(int i=0;i<al.size();i++){
Apple apple = (Apple) al.get(i);
System.out.println(apple.getid());
}
}
}

    在这里我们实际上存入ArrayList是Object对象,因为所有的类都继承自Object类,所以这里实际上不光可以添加Orange对象还可以添加任意类型的对象。我们在get()获取数据的时候,我们以为获取的是一个Apple对象,实际上获取的是一个Obejct的引用,然后强制转换成我们需要的对象。在这里我们强制的将从ArrayList中取出的Object引用强制转换成Apple类型,那么当遇到这个对象类型实际上是Orange类型时,就会发生类型转换错误。所以说这种创建形式非常不安全。报错如下:

png1

    JavaSE5之后引入了泛型的概念,这个概念应用在这种不确定类型的对象集合中最好不过。例如我们如果想创建一个Apple类型的容器,那么就可以使用ArrayList 其中<>括起来的是类型参数(可以有多个),它指定了这个容器可以保存的数据类型,在进入容器之前就限制了对象的类型,所以在取出数据的时候就不会发生类型转换错误了。相对来说是更加安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.chenxyt.java.practice;

import java.util.ArrayList;

class Apple{
private static long counter;
private final long id = counter++;
public long getid(){
return id;
}
}
class Orange{

}
public class AppleAndOrangesWithoutGeneric {
public static void main(String[] args) {
ArrayList<Apple> al = new ArrayList<Apple>();
for(int i=0;i<3;i++){
al.add(new Apple());
// al.add(new Orange());
}
for(Apple apple :al){
System.out.println(apple.getid());
}
}
}

    现在你可以阻止将Orange对象加入到Apple容器中了,并且我们可以使用for-each循环获取容器中的内容。实际上当我们固定了一种泛型数据参数,正如前面几章说到的,继承的时候基类收发的消息,子类也可以收发,所以泛型同样支持向上转型,我们可以传递其导出类参数。

二、基本概念

    Java容器的用途是“保存对象”,并将其划分为两个不同的概念。

    Collection:一个独立的元素序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存对象,而Set不能有重复的元素。Queue按照排队的规则来确定对象的产生顺序。

    Map:一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数组查找值,所以某种意义上讲它是将对象与数字进行了绑定。

    尽管并非总是这样,但是理想情况下我们都是与这些接口打交道。比如,我们可以像下边这样创建一个List:

1
List<Apple> apples = new ArrayList<Apple>();

    这里ArrayList已经被向上转型为List,使用接口的目的是如何修改它的实现,这里实现由ArrayList完成。这种方式并非永远有效,会带来一个其它的问题,ArrayList类可能新添加了其它的方法,因此当它向上转型为List时,可能会是不完善的。因此如果我们要使用这类方法,那就需要使用更加确切的类型。

三、添加一组元素

    java.util包中Arrays和Collections都有很多实用的方法,可以在一个Collection中添加一组元素。
Arrays.asList()方法接受一个数组或者一组用逗号分隔的数据元素列表,并将其转化成一个List对象,但是实际底层是Array数组,因此该方法转换之后的对象不支持变更长度。
    Collection.addAll()方法接受一个Collection对象,以及一个数组或者是一组用逗号分隔的元素列表,将后者添加到前者对象中。
    下边的例子展示了上边两种方法,以及传统的Collection.addAll()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
Integer[] moreInts = {6,7,8,9,10};
collection.addAll(Arrays.asList(moreInts));
Collections.addAll(collection,11,12,13,14);
Collections.addAll(collection,moreInts);
List<Integer> list = Arrays.asList(15,16,17,18);
list.set(1,29);
//run error 数组不支持变更长度
list.add(21);
}
}

    Collection的构造器可以传递另一个Collection用来初始化,但是这种方式不如定义个空的Collection然后使用addAll的形式添加数据灵活

四、容器的打印

    基本类型的容器都带有toString()的方法,以便打印。如下示例介绍了几种基本类型的容器的打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.chenxyt.java.practice;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
public class PrintingContainers {
static Collection fill(Collection<String> collection){
collection.add("one");
collection.add("two");
collection.add("three");
collection.add("four");
collection.add("five");
collection.add("five");
return collection;
}
static Map fill(Map<String,String> map){
map.put("one","ONE");
map.put("two","TWO");
map.put("three","THREE");
map.put("four","FOUR");
map.put("five","FIVE");
map.put("five","FIVE");
return map;
}
public static void main(String[] args) {
System.out.println("ArrayList===" + fill(new ArrayList<String>()));
System.out.println("LinkedList===" + fill(new LinkedList<String>()));
System.out.println("HashSet===" + fill(new HashSet<String>()));
System.out.println("TreeSet===" + fill(new TreeSet<String>()));
System.out.println("LinkedHashSet===" + fill(new LinkedHashSet<String>()));

System.out.println("HashMap===" + fill(new HashMap<String,String>()));
System.out.println("TreeMap===" + fill(new TreeMap<String,String>()));
System.out.println("LinkedHashMap===" + fill(new LinkedHashMap<String,String>()));
}
}

运行结果:

png2

    这里展示了Java容器中的两种主要的类型:Collection和Map,Collection又包括List和Set他们每个位置只能保存一个数据。而Map保存数据的形式则是使用键值对“key-value”的形式。List:以特定的顺序保存数据,Set:集合中的元素都不能重复。Collection还包括另外一种Queue,它要求元素只能从集合的一端进入,从另一端取出。
    从本例的输出结果可以看出,默认的容器带有的toString方法,可以很好的将容器中的数据展示出来。Collection使用[]括起来,Map使用{}括起来,键值对使用=连接。
    接着分析打印输出,ArrayList和LinkedList都是List类型,它们能够按照元素的填入顺序进行打印。区别在于执行某些操作时候的性能不同,而且LinkedList的功能要多于ArrayList,这在后文介绍。
    HashSet、TreeSet、LinkedHashSet都是Set类型,每个相同的元素只保存一个,HashSet使用了相当复杂的存储结构,后文会介绍,因此HashSet的存储顺序没有实际意义。TreeSet按元素比较结果的升序排序,LinkedHashSet与List相同,按照元素的插入顺序进行了排序。
    Map也可以称作是关联数组,可以使用key查找对应的value,同时可以不用关心它们的大小。Map会自动的扩容。同时也不需要关心打印的顺序。

五、List

    List将元素按照插入的顺序排列起来,它在Collection的接口中增加了新的功能,使得可以在List的中间插入和删除元素。List有两种类型如上一节介绍:
    1.ArrayList 优势在于随机访问的速度很快,但是在List中间插入和删除元素比较慢。
    2.LinkedList 与上基本相反,它的特性集较ArrayList更大。
    下面的一个例子展示List的一些特性,首先有个枚举类型,它有几个常量,然后是一个向List中增加数据的方法,这个方法我们使用的是ArrayList,注意前边说到,ArrayList增删操作性能消耗很大,如果我们的程序设计中出现了大量的ArrayList插入操作,那么可能会导致性能的降低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.chenxyt.java.practice;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
enum Pet{
Pet,Rat,Manx,Mutt,Pug,Cymric,Humaster
}
public class Pets {
public static List<Pet> arrayList(int n){
List<Pet> pets = new ArrayList<Pet>();
for(int i=0;i<n;i++){
Random rand = new Random();
int j = rand.nextInt(n);
switch(j){
case 0:
pets.add(Pet.Pet);
break;
case 1:
pets.add(Pet.Rat);
break;
case 2:
pets.add(Pet.Manx);
break;
case 3:
pets.add(Pet.Mutt);
break;
case 4:
pets.add(Pet.Pug);
break;
case 5:
pets.add(Pet.Cymric);
break;
case 6:
pets.add(Pet.Humaster);
break;
default:
break;
}
}
return pets;
}
}

    然后是一个List的操作类,相关操作的注释已经标注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.chenxyt.java.practice;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class ListFeature {
public static void main(String[] args) {
Random rand = new Random(47);
List<Pet> pets = Pets.arrayList(7);
//基本的List展示
System.out.println("1:" + pets);
pets.add(Pet.Humaster);
//List可以自动扩容 向其中添加的元素会被追加到最后
System.out.println("2:" + pets);
//a.contains(b)方法判断 集合a中是否包含b 返回的是boolean类型
System.out.println("3:" + pets.contains(Pet.Humaster));
//remove 移除指定的元素
pets.remove(Pet.Humaster);
Pet p = pets.get(2);
//get(2)获取指定位置上的元素 下标从0开始 indexOf(p) 返回指定元素的下标
System.out.println("4:" + p + " " + pets.indexOf(p));
Pet cymric = Pet.Cymric;
//indexOf(p) 返回指定元素的下标 如果没有则返回-1 存在多个则返回第一个
System.out.println("5:" + pets.indexOf(cymric));
//remove(p) 移除指定元素 返回boolean类型 不存在为false 存在多个则移除第一个
System.out.println("6:" + pets.remove(cymric));
System.out.println("7:" + pets.remove(p));
System.out.println("8:" + pets);
//在下标为3的位置插入元素,后边的元素顺序后移
pets.add(3,Pet.Mutt);
System.out.println("9:" + pets);
//截断 下标为1到4的元素 包括1但是不包括4
List<Pet> sub = pets.subList(1,4);
System.out.println("subList:" + sub);
//a.containsAll(b) a集合是否包含b集合中的全部元素 返回boolean类型
System.out.println("10:" + pets.containsAll(sub));
//排序
Collections.sort(sub);
System.out.println("sorted subList:" + sub);
System.out.println("11:" + pets.containsAll(sub));
//随机排序
Collections.shuffle(sub,rand);
System.out.println("suffle subList:" + sub);
System.out.println("12:" + pets.containsAll(sub));
List<Pet> copy = new ArrayList<Pet>(pets);
sub = Arrays.asList(pets.get(1),pets.get(4));
System.out.println("sub:" + sub);
//a.retainAll(b) a中保留所有与b的交集部分
copy.retainAll(sub);
System.out.println("13:" + copy);
copy = new ArrayList<Pet>(pets);
copy.remove(2);
System.out.println("14:" + copy);
//移除所有的元素
copy.removeAll(sub);
System.out.println("15:" + copy);
//将下标为1的元素更换为指定元素
copy.set(1,Pet.Pug);
System.out.println("16:" + copy);
copy.addAll(2,sub);
System.out.println("17:" + copy);
//判断是否为空 返回boolean类型
System.out.println("18:" + pets.isEmpty());
//清空数据
pets.clear();
System.out.println("19:" + pets);
System.out.println("20:" + pets.isEmpty());
pets.addAll(Pets.arrayList(4));
System.out.println("21:" + pets);
//将List转换成数组
Object[] o = pets.toArray();
System.out.println("22:" + o[3]);
}
}

运行结果:

png3

    因为数据的插入是随机的,所以后边对应的操作也是随机结果。相关操作已经写在注释中,这里不再阐述。

六、迭代器

    迭代器(也是一种设计模式),是一种应用在容器之上的设计。它是一个对象,它的作用是遍历并选择序列中的对象,而不需要确定该序列的底层机构。也就是说,我们使用迭代器的目的是解决了不同容器之间的互通性问题。因此迭代器通常被称作是“轻量级对象”,创建它的代价较小。因此通常有些奇怪的限制。例如:Java中的迭代器只能单向移动,并且它通常:
    1、使用方法iterator()要求容器返回一个Iterator,Iterator将准备好返回容器的第一个元素。
    2、使用next()方法获得容器中的下一个元素。
    3、使用hasNext()方法检查容器中是否还有下一个元素。
    4、使用remove()方法将迭代器中新近返回的元素删除。
    我们继续使用上节的Pets类进行Iterator的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;
import java.util.Iterator;
import java.util.List;
public class SimpleIterator {
public static void main(String[] args) {
List<Pet> pets = Pets.arrayList(7);
Iterator<Pet> it = pets.iterator();
System.out.println("1:" + pets);
System.out.println("2:" + it);
while(it.hasNext()){
Pet p = it.next();
System.out.println("--->" + p);
}
it = pets.iterator();
for(int i = 0;i<7;i++){
it.next();
it.remove();
System.out.println(pets);
}
}
}

运行结果:

png4

    需要注意的是,第二行我们打印Iterator时并没有像打印其它对象时把数据打印出来,而是只打印了这个迭代器底层容器的类型ArrayList。还有就是迭代器对象第一次.next之后获取的是第一个元素,remove方法移除的是next方法获取到的元素,所以remove之前必须要调用next方法。在同一方法中使用Iterator时,要注意前面使用过之后,Iterator的位置会发生变化。
    现在我们换用其它类型的容器来测试迭代器的作用,这里有个display()方法,它不考虑任何容器的类型来进行打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.chenxyt.java.practice;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.TreeSet;
public class SimpleIterator {
public static void display(Iterator<Pet> it){
System.out.println("===" +it.getClass().getName()+ "===");
while(it.hasNext()){
Pet p = it.next();
System.out.println(p);
}
}
public static void main(String[] args) {
ArrayList<Pet> pets = (ArrayList<Pet>) Pets.arrayList(7);
LinkedList<Pet> lkp = new LinkedList<Pet>(pets);
HashSet<Pet> hs = new HashSet<Pet>(pets);
TreeSet<Pet> ts = new TreeSet<Pet>(pets);
LinkedHashSet<Pet> lhs = new LinkedHashSet<Pet>(pets);
display(pets.iterator());
display(lkp.iterator());
display(hs.iterator());
display(ts.iterator());
display(lhs.iterator());
}
}

运行结果:

png5

    还有一种比Iterator更加强大的迭代器ListIterator,这个迭代器功能更全,可以向前或者向后移动,也可以使用set()方法替换它最后访问的元素(使用方式如Iterator的remove方法,要先指定访问元素),同时它还可以使用listIterator(n)方法直接指定到第n个元素的ListIterator。下面的示例展示了ListIterator的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.chenxyt.java.practice;
import java.util.List;
import java.util.ListIterator;
public class SimpleListIterator {
public static void main(String[] args) {
List<Pet> pets = Pets.arrayList(7);
ListIterator<Pet> lit = pets.listIterator();
System.out.println(pets);
System.out.println(lit);
System.out.println("=====Next=====");
while(lit.hasNext()){
int index = lit.nextIndex();
Pet p = lit.next();
System.out.println("Index:" + index + "," + "Pet:" + p );
}
System.out.println("=====Previous=====");
lit = pets.listIterator(7);
while(lit.hasPrevious()){
int index = lit.previousIndex();
Pet p = lit.previous();
System.out.println("Index:" + index + "," + "Pet:" + p );
}
System.out.println("=====Update Set=====");
lit = pets.listIterator();
while(lit.hasNext()){
Pet p = lit.next();
lit.set(Pet.Humaster);
}
System.out.println(pets);
}
}

运行结果:

png6

    如上是ListIterator的一些特性,相比Iterator,功能更加全面了一些。需要注意的是,nextIndex和previous不会发生移位操作。

七、LinkedList

    LinkedList与ArrayList相同都是实现了List接口,但是它在增删的时候效率较高,在随机访问的时候效率略低。LinkedList还增加了其作为栈、队列、双端队列的操作方法。这些方法有的只是名字有差异,或者是返回值有差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.chenxyt.java.practice;
import java.util.LinkedList;
public class LinkedListFeatures {
public static void main(String[] args) {
LinkedList<Pet> pets = new LinkedList<Pet>(Pets.arrayList(7));
System.out.println(pets);
//以下两个方法都返回容器的第一个元素 在容器为空的时候抛异常
System.out.println("pets.getFirst()---》" + pets.getFirst());
System.out.println("pets.elements()---》" + pets.element());
//与上两个方法相同 区别在于为空时返回null
System.out.println("pets.peek()---》" + pets.peek());
//以下两个方法移除列表的第一个元素 在容器为空的时候抛出异常
System.out.println("pets.remove()--->" + pets.remove());
System.out.println("pets.removeFirst()--->" + pets.removeFirst());
//与上两个方法相同 区别在于容器为空时返回null
System.out.println("pets.pool()--->" + pets.poll());
System.out.println(pets);
//在容器第一个位置加入新的元素 其它元素依次后移
pets.addFirst(Pet.Manx);
System.out.println("After addFirst()" + pets);
//以下两个方法在容器尾部插入新元素
pets.add(Pet.Humaster);
System.out.println("After add()" + pets);
pets.addLast(Pet.Pug);
System.out.println("After addLast()" + pets);
//offer 是针对queue 在尾部插入数据 add是针对list 在尾部插入数据
pets.offer(Pet.Rat);
System.out.println("After offer()" + pets);
//移除最后一个并返回该元素
System.out.println("pets.removeLast()" + pets.removeLast());
System.out.println("After removeLast()" + pets);
}
}

运行结果:

png7

八、Stack

    “栈”通常是指“后进先出的容器”,比如装羽毛球的桶,最后放进去的羽毛球可以第一个被拿出来,因为另一端也就是栈底是封闭的。LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用。这里我们使用一个真正的Stack,内部使用LinkedList来实现它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.chenxyt.java.practice;
import java.util.LinkedList;
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<T>();
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();
}
public String toString(){
return storage.toString();
}
}

    这里使用泛型来告诉编译器这个Stack是持有参数化类型T的容器,这个Stack是使用LinkedList实现的,而LinkedList也被告知是使用了T类型的对象。peek()方法返回栈顶元素,但是并不是移除。而pop()方法这里是弹出栈顶元素,也就是移除了栈顶元素。如果我们只是需要栈的行为,而不需要其它无关的行为方法,那么这里使用继承就显然不合适了。后边会讨论在Java1.0中,java.util.Stack这个类的设计。

    下面演示了我们如何使用上边这个新的Stack类:

1
2
3
4
5
6
7
8
9
10
11
12
package com.chenxyt.java.practice;
public class StackTest {
public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
for(String s : "My Dog has fleas".split(" ")){
stack.push(s);
}
while(!stack.empty()){
System.out.print(stack.pop() + " ");
}
}
}

    这里使用了push方法将字符串“My Dog has fleas”用空格分开的单词压入栈中,然后使用pop方法弹出栈顶元素。因为pop方法调用之后会移除栈顶元素,所以会依次弹出栈中的所有元素。在这里我们使用了自己定义的Stack,如果我们导入了java.util.Stack类的话,那么我们这样使用可能会产生命名冲突的现象,解决方法是我们在实例化的时候使用完整的类名,或者修改我们自己定义的Stack类名。

九、Set

    Set不保存重复的元素(至于如何判断元素是否重复,则较为复杂,稍后便会看到),Set最常见的使用是判断对象的归属性,即判断某个对象是否在Set中。正因为如此,Set的查询效率就显得尤为重要了,所以通常会使用HashSet来实现所需要的功能,它对查询专门做了优化。

    Set具有与Collection完全一样的接口,我们可以说Set就是Collection,这是表现了不同的功能,这是Java中继承与多态思想的展现。Set中更加复杂的问题后边17章会介绍。

    下面示例使用了HashSet存放Integer对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.chenxyt.java.practice;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class SetofInteger {
public static void main(String[] args) {
Random random = new Random(47);
Set<Integer> set = new HashSet<Integer>();
for(int i=0;i<10000;i++){
set.add(random.nextInt(30));
}
System.out.println(set);
}
}

运行结果:

png8

    set中插入了0-29的随机数,插入了10000次,但是从打印结果可以看出,只保证了每个数据只出现了一次。而且似乎插入的顺序并没有规律可寻。这是因为出于查找速度的考虑,HashSet使用了散列,将在17章中介绍。HashSet使用散列存储,TreeSet使用红黑树存储,LinkedHahSet因为查询速度的原因也使用了散列,但是它看起来像是使用了链表来进行存储,因为它保证了元素的插入顺序。

    注意:插入顺序与元素的顺序的区别,插入顺序是我们在容器中添加元素时的顺序,而元素的顺序说的是按照某种规则比如从小到大或者从大到小的顺序,也就是元素的结果。当然,上边的示例如果我们相对元素的结果进行排序,那么可以使用TreeSet来替换,因为LinkedHashSet只是保证了元素的插入顺序。

    Set 常用的方法是使用contains()判断是否存在某元素,当然还有一些其它的顾名思义的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.chenxyt.java.practice;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class SetOperations {
public static void main(String[] args) {
Set<String> set1 = new HashSet<String>();
Collections.addAll(set1,"A B C D E F G H I J K L".split(" "));
set1.add("M");
System.out.println("H:" + set1.contains("H"));
System.out.println("N:" + set1.contains("N"));
Set<String> set2 = new HashSet<String>();
Collections.addAll(set2,"H I J K L".split(" "));
System.out.println("set2 in set1:" + set1.containsAll(set2));
set1.remove("H");
System.out.println("set1: " + set1);
System.out.println("set2 in set1:" + set1.containsAll(set2));
set1.removeAll(set2);
System.out.println("set2 removed from set1:" + set1);
Collections.addAll(set1,"X Y Z".split(" "));
System.out.println("XYZ added to set1:" + set1);
}
}

运行结果:

png9

十、Map

    Map可以将一个对象映射到另一个对象上组建一种一对一的键值关系。比如可以设计这样一个程序来验证Java中Random的随机性,理想情况下,Random对每个随机数产生的概率是相同的。我们测试这一理论,于是定义一个Map,键来表示随机出现的数字,值来表示该数字出现的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.chenxyt.java.practice;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class RandomTest {
public static void main(String[] args) {
Random random = new Random(47);
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int i =0;i<1000000;i++){
int x = random.nextInt(20);
Integer freq = map.get(x);
map.put(x,freq == null?1:freq+1);
}
System.out.println(map);
}
}

运行结果:

png10

png11

    20个数字随机1000000次,每个数字出现的概率为1/50000,由运行结果可以看出,基本符合这一理想情况下的推论。上述main()方法中所用的map.get()方法,获取指定键对应的值,如果该键不存在则返回null。Map同样具有判断键值是否存在的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.chenxyt.java.practice;
import java.util.HashMap;
import java.util.Map;
public class PetMap {
public static void main(String[] args) {
Map<String,Pet> petMap = new HashMap<String,Pet>();
petMap.put("MyCat",Pet.Cymric);
petMap.put("MyDog",Pet.Humaster);
petMap.put("MyPig",Pet.Mutt);
System.out.println(petMap);
Pet p = petMap.get("MyDog");
System.out.println(p);
System.out.println(petMap.containsKey("MyDog"));
System.out.println(petMap.containsValue(Pet.Cymric));
}
}

运行结果:

png12

    Map与数组或者其它的Collection一样,可以扩展到多维角度,即key是String类型,value可以重新定义为一个容器。比如上边的示例改造为一个动物有多个名字。那么你需要的就是一个Map<String,List>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MapOfList {
public static void main(String[] args) {
Map<String,List<Pet>> petList = new HashMap<String,List<Pet>>();
petList.put("MyDog",Arrays.asList(Pet.Cymric,Pet.Humaster,Pet.Mutt));
System.out.println(petList);
System.out.println("key:" + petList.keySet());
System.out.println("value:" + petList.values());
for(String s:petList.keySet()){
System.out.print(s + ":");
for(Pet p:petList.get(s)){
System.out.print(p + " ");
}
}
}
}

运行结果:

png13

    上述代码中还展示了.keySet()方法和.values()方法,分别返回Map中所有的键和所有的值。

十一、Queue

    队列是一个典型的先进先出的容器,就像是一个两端打开的管子,从一端放进去的物品,从另一端取出,并且最先取出的是最先放进去的物品。因此队列的取出顺序往往是与插入顺序相同的。正因为队列这种顺序特性,它常常被作为一种可靠的将对象从程序的某个区域发送到另一个区域的途径。LinkedList实现了Queue的接口,因此LinkedList可以当做是Queue的一种实现,这是面向对象编程中继承与多态思想的体现。将LinkedList向上转型为Queue,下面的示例将展示Queue接口中与Queue相关的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.chenxyt.java.practice;
import java.util.LinkedList;
import java.util.Queue;
public class QueueDemo {
public static void printQ(Queue queue){
//不移除的情况下返回队列头部元素 element()方法相同 peek为空返回null element为空抛出异常
while(queue.peek()!=null){
//remove()移除头元素 为空抛出异常 poll 为空返回null
System.out.println(queue.remove() + " ");
}
}
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
for(int i = 0;i<5;i++){
//元素插入队尾
queue.offer(i);
}
printQ(queue);
Queue<Character> qc = new LinkedList<Character>();
for(char c:"QUEUEDEMO".toCharArray()){
qc.offer(c);
}
printQ(qc);
}
}

运行结果:

png14

    offer方法是队列的相关方法之一,它在队列允许的情况下,将元素插入到队列的尾部,或者返回false,peek和element方法都是在不删除的情况下返回队列的第一个元素,peek在队列为空时返回null,element在队列为空抛NoSuchElementException,poll和remove删除当前队列的第一个元素,如果为空poll返回null,remove抛NoSuchElementException异常。

    先进先出描述了最典型的队列规则,在某些情况下,队列还需要弹出当前最需要的元素,这种队列称作是优先级队列。优先级队列每个元素都具有一个执行的优先级,也就是这个元素何时弹出与何时插入没有必然的联系。PriorityQueue添加到Java SE5中,是为了这种优先级形式自动实现。当我们在PriorityQueue上调用了offer()方法来插入一个对象时,这个对象在队列中就会被重新排序。默认的排序是对象在队列中的自然顺序。但是你可以通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保你在调用peek、poll、remove等方法时获取的是当前队列中优先级最高的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
public class QueueDemo {
public static void printQ(Queue queue){
//不移除的情况下返回队列头部元素 element()方法相同 peek为空返回null element为空抛出异常
while(queue.peek()!=null){
//remove()移除头元素 为空抛出异常 poll 为空返回null
System.out.println(queue.remove() + " ");
}
}
public static void main(String[] args) {
List<Integer> ints = Arrays.asList(25,22,20,18,14,9,3,1,5,6,14,18,22,23,25);
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(ints.size(),Collections.reverseOrder());
queue.addAll(ints);
printQ(queue);
}
}

运行结果:

png15

    我们使用了JavaSE5中定义的reverseOrder反序定义了PriorityQueue的优先级。

十二、Collection和Iterator

    Collection是描述所有序列容器共性的根接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.chenxyt.java.practice;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class InterfaceVsIterator {
public static void display(Iterator<Pet> it){
System.out.println("IT-P:");
while(it.hasNext()){
Pet p = it.next();
System.out.print(p + ",");
}
System.out.println("");
}
public static void display(Collection<Pet> collection){
System.out.println("CL-P:");
for(Pet p:collection){
System.out.print(p + ",");
}
System.out.println("");
}
public static void main(String[] args) {
List<Pet> petList = Pets.arrayList(7);
Set<Pet> petSet = new HashSet<Pet>(petList);
Map<String,Pet> petMap = new LinkedHashMap<String,Pet>();
String[] names = ("A,B,C,D,E").split(",");
for(int i =0;i<names.length;i++){
petMap.put(names[i],petList.get(i));
}
display(petList);
display(petSet);
display(petList.iterator());
display(petSet.iterator());
System.out.println(petMap);
System.out.println(petMap.keySet());
display(petMap.values());
display(petMap.values().iterator());
}
}

运行结果:

png16

    从运行结果可以看出,使用Collection和Iterator看起来没有什么区别,通常情况下,使用Collection要更方便一些。

十三、Foreach与迭代器

    foreach语法可以应用在数组中,也可以应用在任何Collection对象,之所以能够这样,是因为JavaSE5引入了新的被称为Iterable的接口,该接口包含了一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。因此如果你创建了任何实现Iterabel的类,都可以将它应用在foreach中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.chenxyt.java.practice;

import java.util.Iterator;

public class IterableClass implements Iterable<String> {
protected String[] words = ("And that is how we know the earth").split(" ");
@Override
public Iterator<String> iterator() {
// TODO Auto-generated method stub
return new Iterator<String>(){
private int index = 0;
public boolean hasNext(){
return index<words.length;
}
public String next(){
return words[index++];
}
public void remove(){
//---
}
};
}

public static void main(String[] args) {
for(String s :new IterableClass()){
System.out.print(s + " ");
}
}
}

运行结果:

png17

    iterator()方法返回的实现了Iterator 的匿名内部类的实例,该匿名内部类可以遍历数组中的所有单词。在main()中,你可以看到IteratorClass确实可以用于foreach语句中。

十四、总结

    容器也就是集合,是Java基础中很重要的一个模块,本篇文章了解了基础的容器使用方式。Java提供了多种容器持有对象的方式:
    1.数组将数字也就是下标与内容关联,查询时不需要进行类型转换,缺点是大小一旦确定,则不能被改变。
    2.Collection保存单一的元素,Map保存相关联的键值对。
    3.像数组一样,List也建立数字索引与对象的关联,因此数组和List都是排序好的容器。
    4.如果要进行大量的随机访问,就要使用ArrayList,如果要进行大量的插入删除操作,就要使用LinkedList。
    5.各种队列与栈的操作,由LinkedList提供支持。
    6.Map是一种将对象与对象进行关联的容器,HashMap设计用来快速访问,TreeMap保持键始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入的顺序,但是也通过散列提供快速访问的能力。
    7.Set不接受重复的数据,HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。LinkedHashSet以插入顺序保持元素。

    简单的容器分类:

png18

【Java编程思想】十一:内部类

发表于 2018-11-15 | 分类于 学习笔记 | 阅读次数:

一、创建内部类

    将一个类定义在另一个类的内部,这就是内部类。内部类与组合是不同的概念。

    创建一个内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.chenxyt.java.test;
public class Parcell {
class Contents{
private int i = 11;
public int value(){
return i;
}
}
class Destination{
private String label;
public Destination(String whereto) {
// TODO Auto-generated constructor stub
label = whereto;
}
String readLabel(){
return label;
}
}
public Contents contents(){
return new Contents();
}
public Destination destination(String s){
return new Destination(s);
}
public void ship(String dest){
Contents c = new Contents();
Destination d = new Destination(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcell p1 = new Parcell();
p1.ship("Inner Class");
Parcell p2 = new Parcell();
Contents c = p2.contents();
Parcell.Destination d = p2.destination("Class Inner");
}
}

    如上我们创建了一个内部类,内部类与其它类的区别在于将类隐藏在了另一个类的内部,同时如contents方法所示,外部类的方法还可以返回一个指向内部类的引用,这也是很常见的一种用法。此外我们看到main()方法中创建的内部类对象是使用外部类的引用关联创建的,这一点在下一节中会说到。

png1

二、链接到外部类

    上边的代码似乎只展示了内部类与其它类名字和组织结构的区别,内部类还有其它的用途。当我们创建了一个内部类对象,此对象就与制造它的外围对象之间有了一种关联,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.chenxyt.java.practice;
interface Selector{
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size){
items = new Object[size];
}
public void add(Object x){
if(next<items.length){
items[next++] = x;
}
}
private class SequenceSelector implements Selector{
private int i = 0;
@Override
public boolean end() {
// TODO Auto-generated method stub
return i == items.length;
}
@Override
public Object current() {
// TODO Auto-generated method stub
return items[i];
}
@Override
public void next() {
// TODO Auto-generated method stub
if(i<items.length){
i++;
}
}
}
public Selector selector(){
return new SequenceSelector();
}
public static void main(String[] args) {
//初始化一个长度为10的数组
Sequence sequence = new Sequence(10);
//数组赋值 每个域赋值为下标编号
for(int i = 0;i<10;i++){
sequence.add(Integer.toString(i));
}
//创建接口对象 返回接口实现的为内部类的对象
Selector selector = sequence.selector();
//判断当前数组下标是否已到最大
while(!selector.end()){
//返回当前数组域的值
System.out.print(selector.current() + " ");
//数组下标+1
selector.next();
}
}
}

    上边的代码很好的展示了这一点,外部类Sequence创建了个数组,内部类SequenceSelector实现了Selector接口获取数组内容并操作数组,可以看到这个类的实现方法访问了外部类的private域。

png2

    所以内部类自动拥有对其外围类所有成员的访问权限,那么这是如何做到的呢?当某个外围类的对象创建一个内部类的对象的时候,这个内部类对象必然会秘密捕获一个外围类对象的引用,也就是这个引用来选择外围类的成员。这里所有的细节都交给了编译器来处理。内部类的对象只能在其与外部类的对象相关联的时候才能被创建(在static方法中),构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错,不过绝大多数情况下这种都不需要我们操心。这里也就解释了上一节关于内部类初始化方式不同的原因。意思就是在static方法中,不能通过直接的new 构造函数的形式创建内部类,因为这种形式没有将内部类与外部类对象做关联,要先创建一个外部类的对象,然后使用该对象的引用创建内部类对象。当然如果内部类是静态的,那么就没有这种要求了。

png3

png4

    我们将上一个示例中的ship方法改成static修饰,原来的使用构造器创建内部类对象的方法就报错了。因为它没有找到关联的外部类对象引用。然后我们把这个内部类Contents改成static修饰,则编译器就不报错了。或者使用main函数中的形式,先创建一个外部类的对象,然后使用这个对象的引用去创建内部类对象。

三、使用.this和.new

    如果我们需要在内部类生成外部类对象的引用,那么可以使用外部类名.this的形式,这里如果只使用this,则返回的是内部类对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.chenxyt.java.practice;
public class DoThis {
void f(){
System.out.println("This is outClass's method");
}
public class Inner{
public DoThis outer(){
return DoThis.this;
//return this;
}
}
public Inner inner(){
return new Inner();
}
public static void main(String[] args) {
DoThis dt = new DoThis();
Inner in = dt.inner();
in.outer().f();
}
}

运行结果:

png5

    如上在outer()方法中使用return this 会提示Dothis类型无法转换成Inner类型。
    如前文说到,我们不能使用new直接创建内部类对象,我们需要使用外部类对象的引用创建,这里可以使用外部类对象的引用.new语法进行创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
public class DoThis {
public class Inner{
public DoThis outer(){
return DoThis.this;
//return this;
}
}
public static void main(String[] args) {
DoThis dt = new DoThis();
Inner in = dt.new Inner();
}
}

    只有内部类是静态内部类或者在非static方法中,我们才可以通过new的形式直接进行创建。

四、内部类与向上转型

    当将内部类向上转型为基类时,尤其是转型为接口时,内部类就有了用武之地。这是因为我们可以使内部类也就是接口的实现完全不可见也不可用,得到的只是基类或者接口的引用,从而更好的隐藏了实现的细节。

    我们先创建两个接口:

1
2
3
4
package com.chenxyt.java.practice;
public interface Destination{
String readLabel();
}
1
2
3
4
package com.chenxyt.java.practice;
public interface Contents {
int value();
}

    然后创建一个类,并在这个类的内部创建两个内部类去实现上边的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.chenxyt.java.practice;
class Parcell4{
private class PContents implements Contents{
private int i = 11;
@Override
public int value() {
// TODO Auto-generated method stub
return i;
}
}
protected class PDestination implements Destination{
private String label;
private PDestination(String whereto) {
// TODO Auto-generated constructor stub
label = whereto;
}
@Override
public String readLabel() {
// TODO Auto-generated method stub
return label;
}
}
public Destination destination(String s){
return new PDestination(s);
}
public Contents contents(){
return new PContents();
}
}
public class TestParcell {
public static void main(String[] args) {
Parcell4 p = new Parcell4();
Contents c = p.contents();
Destination d = p.destination("Inner Class");
System.out.println(d.readLabel());
System.out.println(c.value());
//因为PContents是private 所以不能被访问
//Parcell4.PContents pc = p.new PContents();
}
}

    Parcell4中增加了一些新的东西,首先内部类PContents是private,除了Parcell4没有人能访问它,所以main函数最后一行编译不能通过。其次PDestination是protected的,所以除了该类本身和其子类还有同一个包中的类,其它类不能访问。因此客户端如果想访问这些实现,就受到了限制。不过我们可以看到,main函数的第二、第三行都实现了转型,也就是虽然不可见,但是不影响使用接口的实现。因此private的内部类提供了一种设计思路,通过这种方式完全阻止了依赖任何类型的编码,并且完全隐藏了实现的细节,并且由于不能访问任何新增加的、原本不属于公共接口的方法,因此接口的扩展就是没有价值的了。

png6

五、在方法和作用域内的内部类

    有些时候我们可以将内部类创建在方法的作用域里或者是其它任何地方的作用域中,这么做有两个理由:

    1.如前所示,实现了某个类型的接口,可以创建并返回接口的引用。

    2.要解决一个复杂的问题,需要一个类来辅助解决,但是又不希望这个类是公开的。

    在方法的作用域内部创建的内部类称为局部内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.chenxyt.java.practice;
public class Parcel5 {
 public Destination destination(String s){
  class PDestination implements Destination{
   private String label;
   private PDestination(String whereTo){
    label=whereTo;
   }
   @Override
   public String readLabel() {
    // TODO Auto-generated method stub
    return label;
   }
  }
  return new PDestination(s);
 }
 public static void main(String[] args) {
  Parcel5 p5 = new Parcel5();
  Destination d = p5.destination("Area Inner Class");
  System.out.println(d.readLabel());
 }
}

    如上所示,destination方法中的内部类实现了Destination接口,PDestination类是destination方法内部的类,所以其它地方不能访问。return语句向上转型返回了Destination,它是PDestination类的接口,也就是基类。

png7

    下面的例子展示在任意作用域中使用内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.chenxyt.java.practice;
public class Parcel6 {
private void internalTracking(boolean b){
if(b){
class TrackingSkip{
private String id;
TrackingSkip(String s){
id = s;
}
String getSkip(){
return id;
}
}
TrackingSkip ts = new TrackingSkip("SLIP");
String s = ts.getSkip();
System.out.println(s);
}
//因为内部类在if(b)的作用域内 此处已经超过了作用范围 所以不可以使用
//TrackingSkip ts1 = new TrackingSkip("SLIP1");
}
public void track(){
internalTracking(true);
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
}
}

    上边的例子主要是想在方法中判断如果入参为true则实现一个类的功能,而这个类又不想被外部可见,所以在if中创建了内部类。

png8

六、匿名内部类

    现在有如下个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.practice;
public class Parcel7 {
 public Contents contents(){
  return new Contents(){
   private int i = 11;
   @Override
   public int value() {
    // TODO Auto-generated method stub
    return i;
   }
  };
 }
 public static void main(String[] args) {
  Parcel7 p7 = new Parcel7();
  Contents c = p7.contents();
 }
}

    在此处,contents方法内部要返回一个Contents对象的时候,我们突然加了一个类的定义,这个类没有名字,它实现了Contents接口,也就是我们实际上创建了一个继承自Contents类的匿名类对象,于是这个return对象的引用就变成了一个来自向上转型的Contents引用。上述这个匿名内部类是下面这种形式的一种简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.chenxyt.java.practice;
public class Parcel7 {
public class MyContents implements Contents{
private int i = 11;
@Override
public int value() {
// TODO Auto-generated method stub
return i;
}
}
public Contents contents(){
return new MyContents();
}
public static void main(String[] args) {
Parcel7 p7 = new Parcel7();
Contents c = p7.contents();
}
}

    上述代码使用了默认的无参构造器,下边的示例展示当基类的构造器为有参数的构造器时,匿名内部类应当如何创建:

    基类:

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class Wrapping {
private int i;
public Wrapping(int x){
i=x;
}
public int value(){
return i;
}
}

    匿名内部类:

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class Parcel8 {
public Wrapping wrapping(int x){
return new Wrapping(x){
public int value(){
return super.value() * 11;
}
};
}
}

    只需要传递合适的参数到基类的构造器中即可,虽然Wrapping只是一个普通的实现类,但是他还是被其导出类当做了公共接口来使用。

    如第一个例子所示,匿名内部类中的域可以进行初始化操作。但是,当匿名内部类的域要使用外部对象的引用时,需要强行将函数参数的引用设置为final才可以。

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class Parcel8 {
public Wrapping wrapping(final int x){
return new Wrapping(x){
public int value(){
return super.value() * x;
}
};
}
}

七、嵌套类

    前边我们说的内部类,都是必须要有外部类关联的,也就是这些内部类有个隐式的引用,指向外部类。如果我们不需要这种关联,那么就可以将内部类显示的声明为static的,这种内部类称为嵌套类。嵌套类意味着:

    1.要创建嵌套类的对象,并不依赖外部对象

    2.不能从嵌套类的对象中访问非静态的外围类对象

    嵌套类与普通的内部类还有一个区别,普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段,也不能包含嵌套类,而嵌套类可以包含所有这些东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.chenxyt.java.practice;

public class Parcel11 {
private static class ParcelContents implements Contents{
private int i = 11;
@Override
public int value() {
// TODO Auto-generated method stub
return i;
}
}
protected static class ParcelDestination implements Destination{
private String label;
private ParcelDestination(String whereTo){
label = whereTo;
}
@Override
public String readLabel() {
// TODO Auto-generated method stub
return label;
}
public static void f(){
//
}
static int x = 8;
static class AnotherLevel{
public static void funx(){
//
}
static int y = 11;
}
}
public static Destination destination(String s){
return new ParcelDestination(s);
}
public static Contents contents(){
return new ParcelContents();
}
public static void main(String[] args) {
Contents c = new ParcelContents();
Destination d = new ParcelDestination("dss");
Destination d1 = destination("d1");
}
}

    如上,在main方法中,没有任何Parcel11对象是必须的,我们可以自由的创建内部类对象。
    在正常情况下,不能在接口内部放置任何代码,但是嵌套类可以作为接口的一部分,放在接口中的任何域都是public static的,所以放入的类是嵌套类,甚至我们可以使用此类实现外部接口。如果你想要创建某些公共的代码,并让它可以被某个接口的所有不同实现所共有,那么这种情况就最好不过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.chenxyt.java.practice;
public interface ClassInnerInterface {
void howd();
class Test implements ClassInnerInterface{
@Override
public void howd() {
// TODO Auto-generated method stub
System.out.println("其它不同实现的公共方法");
}
public static void main(String[] args) {
new Test().howd();
}
}
}

    如上我们在内部类中加入了一个main函数,这个main可以用来测试每一个我们编写的类,编译过程中会生成一个独立的class文件,名称如下。如果在生产项目上,我们可以简单的在编译好的文件中删除该class文件即可。

png9

    一个内部类被嵌套多少层并不重要,重要的是它能够透明的访问它所嵌入的外围类的所有成员,即便是被定义为private的域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.chenxyt.java.practice;
class ManyInner {
private void f(){
//---
}
class A{
private void g(){
//---
}
public class B{
void h(){
f();
g();
}
}
}
}
public class MNA{
public static void main(String[] args) {
ManyInner mi = new ManyInner();
ManyInner.A a = mi.new A();
ManyInner.A.B b = a.new B();
b.h();
}
}

    如上可见,在嵌套了多层的内部类B中调用方法f()和方法g()并不需要任何其它附加条件,即便他们被定义为private

八、为什么需要内部类

    一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象,所以可以认为内部类提供了一种进入它外围类的窗口。如果我们只是需要一个对接口的引用,那么为何不使用外围类去实现那个接口呢?答案是如果这样能满足需求,那就需要这样做。内部类实现接口与外部类实现接口的区别在于后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以使用内部类最吸引人的原因:

    每个内部类都能够独立的继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的实现),对于内部类都没有影响。

    内部类有效的实现了“多重继承”,如果在一个类中要使用两个接口,那么使用单一类和内部类看起来没有什么区别(因为单一类可以直接实现多个接口,此处不写例子了),而如果这两个接口换成是抽象类或者是具体的类,那么由于Java不支持多重继承的原因,这里使用单一类显然不能解决问题了,而内部类恰好可以有效的解决这个问题,看似是个“多重继承”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.chenxyt.java.practice;
//要继承的类1
class F{
//---
}
//要继承的类2
abstract class G{
//---
}
//外部类继承A 内部匿名类继承B
class H extends F{
G makeG(){
return new G(){
//--
};
}
}
public class MutiExtends {
static void takeF(F f){
//--
}
static void takeG(G g){
//--
}
public static void main(String[] args) {
H h = new H();
takeF(h);
takeG(h.makeG());
}
}

    如果不是要解决类似上边的“多重继承”问题,那么可以不实用内部类,但是使用内部类还可以获得一些其它的特性。

    1.内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类的对象信息相互独立。
    2.在单个外围类中,可以让多个内部类继承或实现多个基类。
    3.创建内部类的时刻并不依赖于外围对象的创建。(个人不太理解这里,因为前边说了非嵌套类在static方法中使用时,需要关联一个外部对象的引用,不知道这里具体指的是什么。)
    4.内部类并没有令人迷惑的is-a关系,它是一个独立的实体。

九、内部类的继承

    内部类由于与外部类之间有一个隐式的引用关联关系,所以在继承内部类的时候,要显示的说明他们之前的关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.chenxyt.java.practice;
class withInner{
class Inner{
}
}
public class InheritInner extends withInner.Inner{
InheritInner(withInner wi){
wi.super();
}
public static void main(String[] args) {
withInner wi = new withInner();
InheritInner ii = new InheritInner(wi);
}
}

    如上,在子类的构造函数中要传入继承内部类的外部类的引用。

十、内部类可以被覆盖吗

    如果一个类继承了另一个外部类,那么基类中的内部类会发生覆盖吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.chenxyt.java.practice;
class Egg{
private Yolk y;
protected class Yolk{
public Yolk(){
System.out.println("Egg.Yolk");
}
}
public Egg(){
System.out.println("New Egg");
y = new Yolk();
}
}
public class BigEgg extends Egg{
public class Yolk{
public Yolk(){
System.out.println("BigEgg.Yolk");
}
}
public static void main(String[] args) {
new BigEgg();
}
}

    基类中使用了默认的构造函数,并创建了一个内部类对象,子类中“覆盖”了内部类,所以预期的结果应该是使用子类中覆盖之后的内部类构造器。

png10

    但是实际结果显然不是这样的,它还是走了正常的逻辑流程,说明子类并没有覆盖基类中内部类,这两个内部类彼此独立,在自己的命名空间中。当我们想进行类似“覆盖”内部类的功能时,可以明确的继承内部类,然后覆盖其方法。

十一、局部内部类

    定义在方法体中内部类称为局部内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.chenxyt.java.practice;
interface Counter{
int next();
}
public class LocalInnerClass {
private int count = 0;
//局部内部类
Counter getCounter(final String name){
class LocalCounter implements Counter{
public LocalCounter(){
System.out.println("LocalCounter Constructor");
}
@Override
public int next() {
// TODO Auto-generated method stub
System.out.println(name);
return count++;
}
}
return new LocalCounter();
}
//匿名类
Counter getCounter2(final String name){
return new Counter(){
{
System.out.println("Counter Constructor");
}
@Override
public int next() {
// TODO Auto-generated method stub
System.out.println(name);
return count++;
}
};
}
public static void main(String[] args) {
LocalInnerClass lc = new LocalInnerClass();
Counter c1 = lc.getCounter("Local Counter");
Counter c2 = lc.getCounter2("Annoy Counter");
for(int i=0;i<5;i++){
System.out.println(c1.next());
}
for(int i=0;i<5;i++){
System.out.println(c2.next());
}
}
}

    我们分别使用局部内部类和匿名类实现了相同的计数功能。

png11

    它们具有相同的行为能力,那么既然局部类在方法体外部是看不见的,那么为什么不使用匿名类呢?唯一的理由是我们需要一个已命名的构造器,或者重载构造器,而匿名类只能用于实例初始化。也就是说因为没有名字,所以没有可见的构造器。所以使用局部内部类的另一个原因就是需要不止一个该内部类的对象。

十二、内部类标识符

    由于每个类都会产生一个.class文件,其中包含如何创建该类型的全部信息。(此信息产生一个“meta-class”,叫做class对象)所以内部类也一定会有个.class文件,它们有规范的命名规则,外围类加上“$”加上内部类的名字。

    如果是匿名内部类,编译器会简单的产生一个数字作为标识符,如果内部类是嵌套在别的内部类里,那么就继续使用”$”符号。

十三、总结

    1.内部类是定义在一个类内部的类,这个类可以在方法中,也可以在方法外。内部类可以访问到其外部类的所有域。
    2.内部类与外部类之间的关联关系是使用一个隐式的外部类引用,所以在创建内部类时,需要先创建一个外部类引用进行关联。这种形式出现在当内部类不是static修饰并且在static方法域中创建内部类对象引用的时候。
    3.在内部类中,要使用外部类.this才可以返回外部类的对象引用,如果使用this只是返回了内部类的对象引用。当我们创建了一个外部类对象引用时,可以使用引用.new 内部类()的形式创建内部类对象。
    4.内部类可以用来向上转型实现接口,这种方式有效的建立了接口与实现的隔离,可以使实现完全不可见,不可修改。
    5.在方法和作用域里的类,有两个作用,一个是如前所示用来实现接口并返回。另一个原因是我想创建一个类辅助我解决问题,但是不想这个类可见。
    6.使用return new xx(){} 在{}内部定义类的一些域可以创建一个实现或继承xx的匿名类,这个匿名类没有名字,也就没有构造函数。匿名类使用的外部方法引用需要被修饰为final。
    7.如前所示,内部类的创建需要与外部类进行关联。如果我们不需要进行关联,那么可以将内部类修饰为static,这种称为嵌套类。嵌套类与外部类彼此独立。
    8.内部类可以实现类似“多重继承”。
    9.内部类与外部类的引用有关联,所以在继承内部类的时候需要显示的在构造函数中引用外部类的引用,以说明这种关联。
    10.外部类被继承之后,内部类没有发生特别的变化,也就是它不会被覆盖,如果在子类中重新定义同名的内部类,这会被认为是第二个类,与之前的内部类彼此在不同的命名空间,没有关联。
    11.局部内部类是定义在方法中的,作用与匿名类相同,但是有构造函数,可以进行构造函数重载。
    12.所有的类都有标识符,内部类的标识符为外部类名字$内部类名字。

【Java编程思想】1-10章总结

发表于 2018-11-15 | 分类于 学习笔记 | 阅读次数:

一、对象导论

    面向对象编程的思想主要针对的是对象,Java把一切待解决的事物都抽象了出来做为对象,每一个种类的对象都有一个特定的类别,它们有共同的属性和行为。程序是对象的集合,每个程序都由对象组成,每个对象都可以由其它对象组成。对象的特性规定了这个对象能接收什么样的消息,相同类型的对象可以接收相同的消息。消息在对象中的传递是通过接口进行的,并且每个接受消息的对象都可以提供一个特定的服务。
    Java提供三种访问权限控制 public 包内可见包外需要引用、private 类可见、protected 类以及继承类可见。组合跟继承是创建新类的两种形式,组合就是在新类中引入其它对象的实例,表示一种has-a的关系,继承是在基类的基础上衍生出子类的概念,子类具有基类全部的成员跟方法并且子类可以有更多的成员跟方法,继承是一种is-a的关系。
    不同子类继承了同一个基类的同一个方法可以有不同的实现,这是JAVA三大特性多态的一种表现,可以给应该传递父类参数的形参传递一个子类对象的引用,JAVA通过向上转型和动态绑定机制准确的找到子类应该实现的方法。
    Java中所有的类都继承自Object,这种方式的好处是在某些不知道是什么类型的地方可以直接调用Object提供的方法。
    Java中提供了Map、List、Set等集合,这种集合是对象的集合,不同的集合有不同的存储方式。
    Java中的对象都是使用new创建在堆上的,这是一种动态的内存分配方式,Java使用垃圾回收机制自动回收不需要使用的内存。
    Java中提供了异常处理机制,在程序发生异常时会执行另一条路线。
    Java支持多线程的并发式编程以及网络Web程序应用开发。

二、一切都是对象

    Java中的一切都是对象,但是我们操作的是对象的引用。String s;只是创建了一个引用没有进行初始化也就是这个引用没有绑定对象,String s = “abc”;创建了引用s,并进行了初始化绑定了一个字符串对象。
    Java中的对象都是我们使用new关键字来创建的。对象的内存分配地址主要有寄存器:处理器内部的快速处理单元;堆栈:位于随机存储器RAM中,速度仅次于寄存器,堆栈使用堆栈指针操作分配内存,这种分配方式必须制定大小和生命周期,限制了灵活性。Java中对象的引用在堆栈中但是对象并不在堆栈中;堆:同样在RAM中的内存池,区别在于使用的时候不需要指定大小和生命周期,提高了灵活性,Java中使用new关键字创建的对象都在堆中;常量存储:程序代码的内部,在与程序本身分离的系统中,它存在ROM中(只读存储器);非RAM区:如硬盘、数据库等外部存储。
    Java具有较好的平台移植性,因为Java中的基本类型大小在所有平台都相同,Java中的基本类型存储在堆栈中。常见的大小:int 4字节 short 2字节 char 2字节 float 4字节 long 8字节 double 8字节。
    Java中使用Class创建类,类是同一类型对象的集合。对象存在的意义是类的实例化。类中有两种域,一个是属性,一个是方法。
    Java中方法的基本组成包括返回值、方法名、参数、方法体。String toString(Objecct obj){//—};调用形式使用对象引用.方法的形式,如s.toString()。

三、static关键字的用法

    修饰成员变量:被修饰的成员变量为静态成员变量,这个变量不需要对象就可以通过类.变量的形式直接调用,这个变量只有在第一次使用的时候加载分配内存一次,后边再次用到时不进行加载内存分配。
    修饰成员方法:被修饰的成员方法为静态方法,这个方法可以直接通过类.方法的形式调用。这个最直接的应用场景就是在使用单例模式的时候,我们想通过一个方法获取对象,而此时我们还没有对象可以调用这个方法,因此这个时候获取对象的方法设置成为static的最好不过了。
    修饰代码块:修饰代码块的作用与前边相同,表示这个块的内部都是静态的。
    静态导入:还有一种不太常用的形式就是在另外一个包的静态方法,可以使用import static导入,从而直接使用这个方法。

四、操作符

    Java跟其他语言一样支持+ - * / % 等操作符,同时优先级计算顺序也是先乘除后加减有括号先计算括号。
    赋值操作符=将操作符右边的值赋给左边的变量,左边不能是常量。如果使用=连接两个对象的引用,那么=左边的对象会被释放。这种现象叫做“别名”现象,避免这种现象的赋值方式为使用引用.域赋值。
    自增自减与其它语言一样,分为前缀式和后缀式,前缀式++a表示这个值先+1然后再进行例如赋值运算等操作,而后缀式a++则表示先取a的值做赋值或者其他操作,然后再+1
    关系操作符==和!=在用于基本类型操作时与其它语言相同,而对于对象操作时,略有不同。==(!=)对于对象的操作是比较引用的地址是否相同。因为是两个不同的对象引用,所以地址肯定不同。Java中Obejct类提供equals()方法,实际上也是比较引用的地址,而继承Obejct类的大多数如String类,都覆盖了这个方法,改为判断对象的类型是否相同以及值是否相同。

    Java中的逻辑操作符只能应用于布尔值之间,逻辑运算存在短路的可能,位运算符不存在短路的可能。左移(<<)位操作符低位补0,右移(>>)位操作符正数高位补0,负数高位补1,无符号右移(>>>)不管正负高位都补0
    三元运算符自带if-else判断分支,A?B:C如果A为真则表达式取B的值,否则取C的值,常用的地方就是给表达式赋值的时候判断是否为null,为null的时候赋值“”。
    +=和+字符串操作符用来连接两个字符串,如果前边为String类型而后边不为String,后边会被尝试转为String。
    Java支持使用(类型A)值的形式将值显示的转换成A类型。
    Java支持全平台迁移,sizeof函数的意义在于计算值的字节长度,然后考虑平台迁移的问题。

五、控制执行流程

    if-else、for、while这些与其它语言没有区别,for-each用来循环获取一个集合中的值。for(A a :Array[A])

六、构造与清理

    Java中使用与类名相同的无返回值的函数作为构造函数进行初始化。当使用new创建对象的时候,就调用了构造函数。类中如果没有显示的指明构造函数,那么默认为无参的构造函数。如果显示的提供了一个且仅有一个的带参数的构造器,那么该类的无参构造器将不可用。
    Java中支持方法的重载,重载是指函数名相同,但是参数类型或者参数个数不同的形式,重载包括构造器重载和普通函数重载。返回值不同的这种重载容易使人疏忽。
    Java中的this关键字有三种用法:一表示当前对象的引用,并返回当前对象,如在构造器中使用return this 返回该类的对象,二是表示该类中的成员变量,如在带有参数的构造器中,形参与成员变量相同,那么此时使用this.a=a 用来区分是把形参赋值给成员变量。三在一个构造器中调用另一个构造器的时候,使用this代替构造器的名称。
    Java中的垃圾回收不一定会发生,一般如果不显示的调用GC,只有在资源快要枯竭的时候才会发生,因为垃圾回收也会占用资源。
    Java会尽量保证每个变量都会被初始化,如果局部变量没有初始化就调用那么会报错,而成员变量没有显示的初始化,编译器会给赋默认值。
    JavaSE5之前,可以使用Object类型的数组来做变参函数,因为所有类型都是Object的子类,SE5之后提供了 …的形式作为变参函数使用。
    枚举在Java中实际上是一个类,枚举是一系列常量值的集合。枚举的另一个使用场景是使用一个只有单一常量值的枚举来实现单例模式。

七、访问权限控制

    Java中提供了包的概念,包是一系列.java的集合,不同包中访问资源需要使用import关键字导入要使用的包。不同包中的类可以重名。
    public:包可见权限,被public修饰的内容同一个包中都可见,当使用了import关键字导入新包的时候,新包中的public域也能被访问。
    private:私有可见,除了当前类,其它都不可见,包括该类的实例化对象也不可访问该类的private域,比如在单例模式中,将类的构造函数私有化,可以有效的控制实例产生的个数。
    protected:继承可见,与private类似,只有自己的类可见,同时支持该类的继承类访问。
一个文件中最多只能有一个被修饰为public的类。

八、复用类

    对象初始化有四种方式:1是在创建对象引用的时候使用new关键字,2是在构造器中进行初始化,3是延迟加载当用到的时候才进行初始化,4是使用实例进行初始化,即用一个确定的实例代替new关键字。
    组合是产生新类的一种复用形式,组合的基本语法是在一个新类中创建旧类的对象,然后使用这个对象。
    继承也是产生新类的一种复用形式,继承使用extends关键字,被继承的类称作基类,继承类称作导出类,导出类拥有基类的全部域跟方法,并且可以根据具体业务增加新的功能。导出类同时可以重载基类的方法。使用继承的主要场景是想使用基类的一部分方法。
    Java中没有明确的提供对代理的直接支持,可以在A类中定义一个方法,然后在这个方法中使用另一个类的对象调用方法,在main函数中通过调用A中定义的方法来实现业务功能。
    一个方法的形参为基类,传递的参数为子类,编译器会将子类转换成基类,这个过程称作向上转型。由于子类是父类的超集,所以向上转型是安全的。
    final关键字用来修饰一个不可被改变的域。用来修饰变量,表示变量值不可被改变,用来修饰参数,表示这个参数指向的对象不可被改变,用来修饰方法,表示这个方法被锁定,不能被重写,用来修饰类,表示该类不能被继承。
    类的资源加载过程:先查找main函数,然后确定这个类是否有基类,有基类向上加载,直到最顶层的基类。然后加载基类中的static域,然后是子类中的static域。这时候必要的资源都已经加载完成,开始对象的创建。先执行基类构造函数,然后按顺序初始化域,最后执行子类构造函数。

九、多态

    直到运行的时候才确定对象引用调用哪个方法这个行为叫做后期绑定,Java中出了static和final修饰的域其它的都是动态绑定。子类跟基类都具有相同的方法但是实现不同这种称为方法的覆盖或者是重写。也就是因为向上转型和动态绑定,Java中传递不同的参数实现了相应的功能,这也就是多态的意义所在。
    私有方法被重写会当做是新的方法。因为私有方法是不可见的。解决问题的方式是避免出现跟父类私有方法重名的方法。
    只有普通的方法是多态的,域和静态方法都不是多态的。
    一个方法的返回值为指定返回值的子类,这种形式称为协变返回类型。

十、接口

    抽象方法,只有方法定义没有方法实现的被abstract修饰的方法称为抽象方法。
    包含有抽象方法的被abstract修饰的类称作是抽象类。抽象类是不安全的,不能够创建对象,同时可以创建一个没有任何抽象方法的类,这个类存在的目的就是不想让这个类创建对象。继承abstract的类,必须实现基类的abstract方法,否则子类也要被定义为abstract类。
    将class关键字替换成interface即创建了一个接口,接口中所有的方法都是只有定义没有具体实现的。所有的域都被隐式的修饰为static final。如果我们想实现一个接口的话使用implements关键字实现这个接口。
    对于形参类型是类的参数,我们只能传递这个类或者是子类,而对于形参是接口类型的方法,则所有使用implements关键字实现了这个接口的类都可以作为参数传递。
    Java不支持多重继承,但是可以实现多个接口。接口也可以继承接口,也可以继承多个接口。
    适配器模式用来将两个不能一起工作的类,适配在一起。
    接口中的域隐式的为static final 这是SE5之前可以用来实现枚举类型。
    接口也支持嵌套接口,在实现一个接口的时候,不需要实现它内部嵌套的接口。并且private接口只能在定义它的类中使用。
    工厂方法模式允许在不同的类型中复用同一段代码。

【Java编程思想】十:接口

发表于 2018-11-14 | 分类于 学习笔记 | 阅读次数:

一、抽象类和抽象方法

    在前边关于多态的例子中,基类方法往往没有具体的实现,它存在的目的是为不同的子类提供统一的方法,通过动态绑定以及向上转型来完成子类需要的功能。为此,我们可以创建一个这样的类,只为子类提供接口,并且不允许这个类实例化对象,我们可以让这个类中的方法返回一个错误信息,但是这样存在一个问题,错误只能在运行时产生并且带来的影响不可预估。Java为我们提供了一个更加明确的方法,称为抽象方法,抽象方法顾名思义它是虚拟存在的,也就是它不能够被执行。这种方法是不完整的,它只有方法的声明,没有方法的实现,为了区分,使用abstract关键字表示一个方法是抽象方法。

1
abstract void f();

    包含抽象方法的类叫做抽象类,如果一个类包含一个或多个抽象方法,那么这个类叫做抽象类,同样使用abstract关键字修饰。抽象类是不安全的,因为它不完整,所以当试图用它实例化一个对象时,编译器会报错。如果一个类继承自一个抽象类,那么它必须为基类中所有的抽象方法提供一个具体的实现,否则这个子类也必须被定义为抽象类。我们也可以定义一个没有任何方法(包括没有抽象方法)的抽象类,这样做的目的是,这个类没有什么实际的意义,同时也不想让它能够创建对象。

    下面一个示例看下抽象类和抽象方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.chenxyt.java.test;
abstract class Instrument{
private int i;
public String what(){
return "Instrumet";
}
public abstract void play(String s);
public void adjust(){};
}
class Wind extends Instrument{
public String what(){
return "Wind";
}
public void play(String s){
System.out.println("Wind.play" + s);
}
public void adjust(){};
}
class Percussion extends Instrument{
public String what(){
return "Percussion";
}
public void play(String s){
System.out.println("Percussion.play" + s);
}
public void adjust(){};
}
class Stringed extends Instrument{
public String what(){
return "Stringed";
}
public void play(String s){
System.out.println("Stringed.play" + s);
}
public void adjust(){};
}
class Brass extends Instrument{
public String what(){
return "Brass";
}
public void play(String s){
System.out.println("Brass.play" + s);
}
public void adjust(){};
}
class WoodWind extends Instrument{
public String what(){
return "WoodWind";
}
public void play(String s){
System.out.println("WoodWind.play" + s);
}
public void adjust(){};
}

public class Music{
static void tune(Instrument i ){
i.play("finish");
}
static void tuneAll(Instrument[] e){
for(Instrument i:e){
tune(i);
}
}
public static void main(String[] args) {
Instrument[] iArray = {
new Wind(),new Percussion(),new Brass(),new Stringed(),new WoodWind()
};
tuneAll(iArray);
}
}

运行结果:

png1

    如上可见,创建抽象类和抽象方法非常的有用,因为他们使类的抽象性更加明确,并告诉用户和编译器打算怎么样使用他们。抽象类还是一个很有用的重构工具,因为他们使得我们可以很容易的将公共方法沿着继承的层次向上移动。

二、接口

    关键字使抽象的概念更加深入了一步。抽象类中可以允许抽象方法和普通方法共存,普通方法存在的目的是为所有继承的子类提供一个相同实现的方法。而接口创建了一个完全抽象的概念,接口内部不存在任何方法具体的实现。所有的实现都交由到实现这个接口的类完成。

    接口使用interface关键字代替class关键字,访问权限控制与一个class相同,接口中可以包含域,但是这些域被隐式的定义为static和final型。

    要想实现一个接口,就需要使用implements关键字显示的指明要实现哪个接口。接口中的方法必须被定义为public方法。实现接口的类要显示的编写实现接口中的所有方法,即便有些方法不需要实现,那也要如同接口一样写一个空的方法体。

    修改上边抽象类的示例为接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.chenxyt.java.test;
interface Instrument{
public void play(String s);
}
class Wind implements Instrument{
public String what(){
return "Wind";
}
public void play(String s){
System.out.println("Wind.play" + s);
}
public void adjust(){};
}
class Percussion implements Instrument{
public String what(){
return "Percussion";
}
public void play(String s){
System.out.println("Percussion.play" + s);
}
public void adjust(){};
}
class Stringed implements Instrument{
public String what(){
return "Stringed";
}
public void play(String s){
System.out.println("Stringed.play" + s);
}
public void adjust(){};
}
class Brass implements Instrument{
public String what(){
return "Brass";
}
public void play(String s){
System.out.println("Brass.play" + s);
}
public void adjust(){};
}
class WoodWind implements Instrument{
public String what(){
return "WoodWind";
}
public void play(String s){
System.out.println("WoodWind.play" + s);
}
public void adjust(){};
}

public class Music{
static void tune(Instrument i ){
i.play("finish");
}
static void tuneAll(Instrument[] e){
for(Instrument i:e){
tune(i);
}
}
public static void main(String[] args) {
Instrument[] iArray = {
new Wind(),new Percussion(),new Brass(),new Stringed(),new WoodWind()
};
tuneAll(iArray);
}
}

运行结果:

png2

三、完全解耦

    只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想将这个方法应用在不在此继承结构中的某个类,那么使用接口将很大程度的放宽这种限制。因此,它可以使我们编写可复用性更好的代码。
    例如,有一个Processor类,它有一个name()方法,还有一个process()方法,该方法接受输入参数,修改输入的值然后进行输出。这个类作为基类被扩展,子类创建各种不同类型的Processor,在本例中,Processor子类通过process()方法修改String对象的值,返回类型可以是协变类型,而非参数类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.chenxyt.java.test;
import java.util.Arrays;
class Processor{
public String name(){
return getClass().getSimpleName();
}
Object process(Object input){
return input;
}
}
class UpCase extends Processor{
String process(Object input){
return input.toString().toUpperCase();
}
}
class DownCase extends Processor{
String process(Object input){
return input.toString().toLowerCase();
}
}
class Splitter extends Processor{
String process(Object input){
return Arrays.toString(input.toString().split(" "));
}
}
public class Apply{
public static void process(Processor p,Object s){
System.out.println("Using Processor:" + p.name());
System.out.println(p.process(s));
}
public static final String S = "Disagreement with beliefs is by definition incorrect";
public static void main(String[] args) {
process(new UpCase(), S);
process(new DownCase(), S);
process(new Splitter(), S);
}
}

运行结果:

png3

    前边学习多态的时候有过类似的例子,Apply.process()方法可以接收Processor类型跟它的子类,并将它应用到了Object对象,然后打印。像这种,根据继承关系,创建一个能够根据所传递的参数对象不同而具有不同行为的方法,称为策略设计模式。这类方法包含索要执行的方法中固定不变的部分(如本例的name()方法),而“策略”包含变化的部分(如本例的process())方法。策略就是传递的参数对象,它包含要执行的代码。这类Processor对象就是一个策略,在main()方法中可以看到三种不同类型的策略应用到Obejct对象上。

    下面有如下4个类,他们看起来也适用Apply.process()

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;
public class Filter{
public String name(){
return getClass().getSimpleName();
}
public WaveForm process(WaveForm input){
return input;
}
}

    Filter类,看上去与Processor类似,都有name()方法和process()方法,区别在于方法的参数类型和返回类型不同。

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class HighPass extends Filter{
double cutoff;
public HighPass(double cutoff){
this.cutoff = cutoff;
}
public WaveForm prcess(WaveForm input){
return input;
}
}

    HighPass类,继承自Filter类

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class BandPass extends Filter {
double lowCutoff,highCutoff;
public BandPass(double lowCutoff,double highCutoff){
this.lowCutoff = lowCutoff;
this.highCutoff = highCutoff;
}
public WaveForm process(WaveForm input){
return input;
}
}

    BandPass类,继承自Filter类

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
public class WaveForm{
private static long counter;
private final long id = counter++;
public String toString(){
return "Wave Form" + id;
}
}

    WaveForm类,前边方法的参数类型和返回类型。

    Filter与Processor类具有相同的内部接口元素(两个类的方法名都相同),但是由于Filter类并非继承自Processor类,因此当Apply.process()方法传入参数Filter的时候,由于Filter类的创建者并不知道要当做Processor类使用,并且它也不能通过向上转型的方式变成Processor类,因此不能将Filter类应用到Apply.process()方法。这主要是因为Apply.process()方法和Processor类的耦合度太高了,已经超出了所需要的程度。这就是Apply.process()方法只能接收Processor类或者其子类,而面对新的类的时候,Apply.process()方法就无能为力了,对其的复用也就被禁止了。

    但是,正如前文所说,如果操作的是接口而不是类的时候,那么这些限制就会变得松动,使得你可以复用接口的Apply.process()方法,下面是修改为接口的版本。

1
2
3
4
5
package com.chenxyt.java.practice;
public interface Processor{
String name();
Object process (Object input);
}

    此时Processor类变成了一个接口,复用代码的形式就是之前继承它的类,可以改为实现它的接口,并且Filter类,也可以编程实现Processor类的接口,这样Apply.process()方法的耦合度就降低了,并且支持了其它的类型。还有一种情况,假如一个类是被发现的,而不是被我们自己创建的,那么这个类就无法实现Processor接口,比如说,如果Filter类是在类库中的类,那么这个类就无法主动实现Processor接口,这时候可以使用适配器模式,在这个类的外部封装一层,作为适配器来实现要实现的接口。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.chenxyt.java.practice;
class FilterAdapter implements Processor{
Filter filter;
public FilterAdapter(Filter filter){
this.filter = filter;
}
public String name(){
return filter.name();
}
public WaveForm process(Object input){
return filter.process((WaveForm)input)
}
}
public class FilterProcessor{
public static void main(String[] args) {
WaveForm w = new WaveForm();
Apply.process(new FilterAdapter(new LowPass(1.0)),w);
Apply.process(new FilterAdapter(new HighPass(2.0)),w);
Apply.process(new FilterAdapter(new BandPass(3.0,4.0)),w);
}
}

    在这种使用适配器的方式中,FilterAdapter的构造器接受了Filter参数,然后生成对应接口Processor的对象。
本节主要的内容是使用接口的方式将只有基类和其子类的使用方法解耦出来,便于程序更好的进行复用。

四、Java中的多重继承

    C++中允许多重继承,并且每一个继承的类都可以有一个实现,Java中是不允许的,Java中可以实现多个接口,每个接口名字在implements后边用逗号隔开,并且,Java中只能继承一个类。下面的例子说明一个具体的类组合实现多个接口产生一个新类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.chenxyt.java.practice;
interface CanFight{
void Fight();
}
interface CanSwim{
void Swim();
}
interface CanFly{
void Fly();
}
class ActionChracter{
public void Fight(){
//---
};
}
class Hero extends ActionChracter implements CanFight,CanSwim,CanFly{
public void Fly(){
//---
};
public void Swim(){
//--
};
}
public class Adventure{
public static void t(CanFight x){
x.Fight();
}
public static void u(CanSwim x){
x.Swim();
}
public static void v(CanFly x){
x.Fly();
}
public static void w(ActionChracter x){
x.Fight();
}
public static void main(String[] args) {
Hero h = new Hero();
t(h);
u(h);
v(h);
w(h);
}
}

    可以看到Hero类组合具体类ActionChracter和另外的三个接口,当通过这种方式将类和接口组合在一起时,这个类必须放在前边,接口放在后边,否则编译器会报错。同时我们注意到,CanFight接口与ActionChracter类中的Fight()方法相同,而且Hero中并没有提供Fight()的具体定义。可以扩展接口,当想要创建对象的时候,所有的定义必须都存在,即使Hero没有显示的定义Fight()方法,由于其继承了ActionChracter类,所以定义随之而来,这使创建对象变成了可能。这里的意思是说,一个类实现了某些接口,这些接口中所有的定义在这个类中必须要有相关的实现(编译器会主动提示),然后因为这个类继承了一个类(ActionChracter),所以如果基类有实现了接口中的方法,那么子类就可以不显示的实现这个方法。(区别在于基类不是实现了这个方法,只是方法签名相同)

    这个例子中,给出的四个方法分别使用接口作为了参数,所以在Hero作为参数传递的时候,它被依次进行了向上转型,Java中的接口设计,使得这项功能并不复杂。这个例子所展示的是使用接口的核心原因:为了能够向上转型为多个基本类型,提升程序的灵活性。使用接口的第二个原因与抽象类相同,防止程序员在使用的过程中创建该类的对象。当然关于这一点是使用抽象类还是接口,当要创建的类中没有任何方法定义和成员变量的定义是,选择接口是合适的,并且当知道某事物应当成为一个基类的时候,那么第一选择是应当使它成为接口。

五、通过继承扩展接口

    接口也可以继承!没错,通过继承可以很容易的在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.chenxyt.java.practice;
interface Monster{
void menace();
}
//新接口继承原来的接口
interface DangerousMonster extends Monster{
void destroy();
}
interface Lethal{
void kill();
}
//实现接口 要依次定义这个接口的方法以及它继承接口的方法 编译器自动补充
class DragonZill implements DangerousMonster{
@Override
public void menace() {
// TODO Auto-generated method stub
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
}
//接口可以多重继承
interface Vampire extends DangerousMonster ,Lethal{
void drinkblood();
}
//继承多个接口 都要把定义实现
class VeryBadVampire implements Vampire{

@Override
public void destroy() {
// TODO Auto-generated method stub

}

@Override
public void menace() {
// TODO Auto-generated method stub

}

@Override
public void kill() {
// TODO Auto-generated method stub

}

@Override
public void drinkblood() {
// TODO Auto-generated method stub

}
}

代码中标注了,接口可以使用extends继承多个,但是这一形式不适用于普通的类。

    这里说到了上边例子中的CanFight类和ActionChracter类都有一个相同的方法,如果方法只是名字相同,参数类型不同,返回类型不同,那么将带来逻辑上很大的问题。因此在继承、实现接口、覆盖或者重载的时候,应尽量避免重名的问题出现

六、适配接口

    接口最吸引人的地方,就是允许同一个接口具有多个不同的实现。简单来说,就是一个接受接口类型的方法,而该接口的实现和向该接口传递的对象取决于方法的使用者。因此常用的方式就是前边的策略模式,此时你编写一个执行某些操作的方法,该方法接受一个同样是你指定的接口,你主要就是声明”你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口“这使你的方法更加灵活。

    这里把我把上边适配接口的例子全部的代码撸了一遍,并分析了一下,具体的关于适配器模式后边会专门再学习一下,此处只是将书中的例子学习了一下

    首先有个Processor接口,该接口有两个方法声明

1
2
3
4
5
package com.chenxyt.java.practice;
public interface Processor{
String name();
Object process(Object input);
}

    然后是个Apply类,这个类有个静态方法process,为了解耦,传递的参数为接口类型,接口的方法作用于一个Object对象

1
2
3
4
5
6
7
package com.chenxyt.java.practice;
public class Apply {
public static void process(Processor p,Object s){
System.out.println("Using Processor" + p.name());
System.out.println(p.process(s));
}
}

这时我们发现了一个Filter类,因为这个类是发现的,所以看上去它跟Procesor接口有相同的方法,只是类型不同,所以可以直接实现该方法,但是由于这个类是已经写好了的。所以它不可以被修改了。

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;
public class Filter{
public String name(){
return getClass().getSimpleName();
}
public Waveform process(Waveform input){
return input;
}
}

    然后是一个Waveform类,这个类作为Filter类process方法的返回值,定义了个toString方法,所以在打印的时候会调用这个toString方法并把输入的内容返回

1
2
3
4
5
6
7
8
package com.chenxyt.java.practice;
public class Waveform {
private static long counter;
private final long id = counter++;
public String toString(){
return "Waveform" + id;
}
}

    然后是Filter类的三个子类,分别实现了父类的process方法

1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class LowPass extends Filter{
double cutoff;
public LowPass(double cutoff) {
this.cutoff = cutoff;
}
public Waveform process(Waveform input){
return input;
}
}
1
2
3
4
5
6
7
8
9
10
package com.chenxyt.java.practice;
public class HighPass extends Filter{
double cutoff;
public HighPass(double cutoff) {
this.cutoff = cutoff;
}
public Waveform process(Waveform input){
return input;
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public class BandPass extends Filter {
double lowCutoff,highCutoff;
public BandPass(double lowCutoff,double highCutoff){
this.lowCutoff = lowCutoff;
this.highCutoff = highCutoff;
}
public Waveform process(Waveform input){
return input;
}
}

    这个时候问题来了,因为Appply类的process方法传参数是接口,这时Filter类已经存在,想直接使用Apply的process方法行不通,想实现Processor接口已经来不及。那么就要使用适配器模式啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.chenxyt.java.practice;

class FilterAdapter implements Processor{
Filter filter = new Filter();
public FilterAdapter(Filter filter) {
this.filter = filter;
}
@Override
public String name() {
// TODO Auto-generated method stub
return filter.name();
}
public Waveform process(Object input) {
// TODO Auto-generated method stub
return filter.process((Waveform)input);
}
}
public class FilterProcessor{
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)),w);
Apply.process(new FilterAdapter(new HighPass(2.0)),w);
Apply.process(new FilterAdapter(new BandPass(3.0,4.0)),w);
}
}

    这里我简单的理解了一下这里的适配器模式,就是写了一个适配器的类,这个类实现了Processor接口,内部接受了Filter对象参数,然后生成了你想要的Processor接口对象,达到了预期的目的。这里有关具体的设计模式分析,后边会继续学习。

七、接口中的域

    在接口中的域,会被自动的隐式转换为static final类型,所以接口就可以很便捷的创建一组常量值,也就是枚举。在JavaSE5之前,没有枚举的概念之前,可以使用接口来创建常量组。

1
2
3
4
5
6
package com.chenxyt.java.practice;
public interface Months{
int JANUARY = 1,FEBRUARY=2,MARCH=3,APRIL=4,
MAY=5,JUNE=6,JULY=7,AUGUST=8,SEPTEMBER=9,OCTOBER=10,
NOVEMBER=11,DECEMBER=12;
}

    当然这种形式在后来已经被enum取代了。因为是final类型,所以必须显示的指定初始化的值,同时因为是static域,所以它们在第一次访问的时候被初始化,并且这些域不属于接口的一部分,它们的值存储在接口的静态存储区域。

八、嵌套接口

    接口可以嵌套在类或者其它的接口中,个人觉得这种设计会使程序变得更加复杂不易读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.chenxyt.java.practice;
class A{
interface B{
void f();
}
public class BImp implements B{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
public class BImp2 implements B{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
public interface C{
void f();
}
class CImp implements C{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
private class CImp2 implements C{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
private interface D{
void f();
}
private class DImp implements D{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
private class DImp2 implements D{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
public D getD(){
return new DImp();
}
private D dRef;
public void reveiveD(D d){
dRef = d;
dRef.f();
}
}
interface E{
interface G{
void f();
}
public interface H{
void f();
}
void g();
//强制必须为public
//private interface I{};
}

public class NestingInterfaces{
public class BImp implements A.B{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
class CImp implements A.C{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
//因为接口D是私有的 所以不能被实现
//class DImp implements A.D{);
class EImp implements E{
@Override
public void g() {
// TODO Auto-generated method stub
}
}
class EGImp implements E.G{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
class EImp2 implements E{
@Override
public void g() {
// TODO Auto-generated method stub
}
class EG implements E.G{
@Override
public void f() {
// TODO Auto-generated method stub
}
}
}
public static void main(String[] args) {
A a = new A();
//D是private 不能实例化
//A.D ad = new A.D();
//getD()方法只能返回D
//A.DImp2 di2 = a.getD();
//private接口的域不能被访问
//a.getD().f();
//可以通过内部返回域的方法获取
A a2 = new A();
a2.reveiveD(a.getD());
}
}

    这里主要要说明的就是private interface接口的作用,就像A.D接口一样,它能够被实现为DImp的一个内部类,也同样可以像DImp2一样实现为public类,但是正如main方法中倒数第4行代码一样,A.DImp2只能被自己使用,因为你无法说你实现了一个private接口D,因此这个实现只是一种形式而已,它可以强制该方法的定义不带有任何类型信息,即不可以向上转型。所以我们在get方法中return new DImp2 的时候并没有获取到预期的值,因为DImp2是一个实现了private接口的public类,最终我们还是通过receiveD方法获取到了相应的实例。
    接口E说明了接口之间的嵌套关系,因为接口内部所有元素都是public的,所以不能指明嵌套在内部的接口为private。NestingInterfaces展示了嵌套接口的几种形式,特别注意的是,在实现一个接口的时候,不需要实现其内部嵌套接口的方法。而且private接口不能在定义它的类外部被实现,比如上述代码中的A.D

九、接口与工厂

    接口是实现多重继承的重要途径,而生成遵循某个接口对象的典型方式就是工厂方法设计模式。由此可见设计模式的重要性,我自己最近也在学习这一块的内容。希望能够有所提高。
    使用工厂方法与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上来说,我们通过这种方式可以将我们的代码与接口的实现完全分离,这就使我们可以透明的将某个实现替换成另一个实现。如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.chenxyt.java.practice;
interface Service{
void method1();
void method2();
}
interface ServiceFactory{
Service getService();
}
class Implementation1 implements Service{
Implementation1() {
// TODO Auto-generated constructor stub
}
@Override
public void method1() {
// TODO Auto-generated method stub
System.out.println("Implementation1 method1");
}
@Override
public void method2() {
// TODO Auto-generated method stub
System.out.println("Implementation1 method2");
}
}
class Implementation1Factory implements ServiceFactory{
@Override
public Service getService() {
// TODO Auto-generated method stub
return new Implementation1();
}
}
class Implementation2 implements Service{
Implementation2() {
// TODO Auto-generated constructor stub
}
@Override
public void method1() {
// TODO Auto-generated method stub
System.out.println("Implementation2 method1");
}
@Override
public void method2() {
// TODO Auto-generated method stub
System.out.println("Implementation2 method2");
}
}
class Implementation2Factory implements ServiceFactory{
@Override
public Service getService() {
// TODO Auto-generated method stub
return new Implementation2();
}
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact){
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(new Implementation1Factory());
serviceConsumer(new Implementation2Factory());
}
}

    这里如果不是使用工厂方法,代码中就要指定Service的确切类型,以便调用合适的构造器。使用工厂方法设计模式的原因是想要创建框架,提高代码的复用性。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.chenxyt.java.practice;
interface Game{
boolean move();
}
interface GameFactory{
Game getGame();
}
class Checkers implements Game{
private int moves = 0;
private static final int MOVES = 3;
@Override
public boolean move() {
// TODO Auto-generated method stub
System.out.println("Checkers moves" + moves);
return ++moves != MOVES;
}
}
class CheckersFactory implements GameFactory{
@Override
public Game getGame() {
// TODO Auto-generated method stub
return new Checkers();
}
}

class Chess implements Game{
private int moves = 0;
private static final int MOVES = 4;
@Override
public boolean move() {
// TODO Auto-generated method stub
System.out.println("Chess move" + moves);
return ++moves != MOVES;
}
}
class ChessFactory implements GameFactory{
@Override
public Game getGame() {
// TODO Auto-generated method stub
return new Chess();
}
}
public class Games {
public static void PlayGame(GameFactory fact){
Game s = fact.getGame();
while(s.move()){
//--
}
}
public static void main(String[] args) {
PlayGame(new CheckersFactory());
PlayGame(new ChessFactory());
}
}

    如果Games类表示一段复杂的代码,那么这种方式就允许你在不同的游戏类型中复用这段代码。

十、总结

    抽象类跟接口是将具体方法更加抽象的一种形式,这一章节主要讲了抽象类、抽象方法的形式以及使用场景,比较重要的一点是关于接口的使用,如何解耦,接口可以多重继承,接口可以嵌套等应用场景。关于这一章节中的设计模式,还要继续深入研究下去。

【Java编程思想】九:多态

发表于 2018-11-13 | 分类于 学习笔记 | 阅读次数:

一、再讨论向上转型

    在第七章中我们说过,对象引用既可以作为它自己本身的类型使用,也可以作为它的基类型使用,这种把某个类型引用作为它的基类型使用的做法被称为向上转型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.chenxyt.java.practice;
class Instrument{
public Instrument() {
//---
}
public void print(){
System.out.println("Instrument-----:");
}
}
class Wind extends Instrument{
public void print(){
System.out.println("Wind-----:");
}
}
public class Music{
public static void play(Instrument i){
i.print();
}
public static void main(String[] args) {
Wind wind = new Wind();
play(wind);
}
}

运行结果:

png1

    Main方法中play方法传递wind引用的时候,不需要做任何类型转换。这样做是允许的,因为Wind自Instrument类继承而来,所以Instrument类的接口必定存在于Wind类中。这种向上转型的特性,避免了重新编写方法带来的程序代码冗余问题。

二、转机

    考虑一个新的问题,当我们有多个子类的时候,编译器是怎样知道我们传递给基类引用的参数是哪个子类呢?比如我们把刚才的程序做个修改,再增加一个子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.chenxyt.java.practice;
class Instrument{
public Instrument() {
//---
}
public void print(){
System.out.println("Instrument-----:");
}
}
class Wind extends Instrument{
public void print(){
System.out.println("Wind-----:");
}
}
class Rain extends Instrument{
public void print(){
System.out.println("Rain-----");
}
}
public class Music{
public static void play(Instrument i){
i.print();
}
public static void main(String[] args) {
Rain rain = new Rain();
play(rain);
}
}

运行结果如下:

png2

    在play方法中,既然做到了向上转型,那么编译器怎样知道类型从哪里转来的呢?即执行哪个对应的print()方法呢?解决这个问题有一个新的概念叫后期绑定,就是程序运行时根据对象的类型来进行绑定,也叫做动态绑定。一种语言要想实现动态绑定,那么它必须具有某种特定的机制来支持它在运行时准确的找到对象引用对应的类型,随着语言的不同这种机制有所不同,但大体上都是在对象中增加了某种类型信息。
    Java中除了static和final之外,其它所有的方法都是后期绑定,所以我们无需显示的去做什么操作,因为动态绑定会自动发生。而前期绑定并不会对性能造成什么影响,使用final修饰的意图是防止被覆盖,并且告诉编译器这个是前期绑定,那么编译器可以更好的为其分配资源。
    上述代码中,不同的子类与基类都有相同的方法(返回值、方法名、参数列表都相同),但是方法体内部不相同,这种操作叫做方法的重写或者方法的覆盖,即子类覆盖了父类方法的实现,当参数传递为子类对象的引用时,虽然看似调用了父类的这个方法,但是实际上由于动态绑定调用了子类的方法,实现了不同的功能。这也就是面向对象编程中多态的意义所在。

方法的重写有如下几点缺陷:

1.重写私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.chenxyt.java.practice;
public class PrivateOverride{
private void f(){
System.out.println("private void f");
}
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride{
public void f(){
System.out.println("public void f");
}
}

我们期望重写了私有方法,但是实际结果并不会重写,编译器会当做一个新的方法执行。

png3

2.域与静态方法:

    实际上只有普通的方法调用可以是多态的,对于域和静态方法都不是多态的。域是在访问的时候编译期进行解析的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.chenxyt.java.practice;
class Super{
public int field = 0;
public int getField(){
return field;
}
}
class Sub extends Super{
public int field = 1;
public int getField(){
return field;
}
public int getSuperField(){
return super.field;
}
}
public class fieldAccess{
public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field" + sup.field + "---sup.GetField" + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field" + sub.field + "---sub.GetField" + sub.getField() + "---sub.GetSuperField" + sub.getSuperField());
}
}

运行结果:

png4

    当Sub对象转型为Super引用时,任何域访问操作都将由对象编译器解析,因此不是多态的,所以第一行第一个值通过直接访问域的形式返回的是0,对于普通方法getField则是多态的,所以第一行第二个值返回1。第二行就比较好理解了,不涉及向上转型的问题,唯一使用了getSuperField方法显示的获取了基类的值。
在本例中为sub.field域和super.filed域分配了不同的内存空间,也就是对于sub来说他有两个field值,一个是它本身的field值,另一个是来自super继承的值,而在sub引用域field时使用的并非是来自super的值,而是它自己本身的默认值。在实际工作中,这种问题基本不会发生,避免问题出现的有效做法是将基类与子类的域分别起不同的名字。

    静态方法由于只与类有关,而不与对象牵连,因此它不存在多态的形式。

三、构造器和多态

    构造器属于类的,它是一个隐式的static方法,因此不存在多态。这里继续分析构造器的调用过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.chenxyt.java.practice;
class First{
public First(){
System.out.println("First---");
}
}
class Second{
public Second(){
System.out.println("Second---");
}
}
class Third{
public Third(){
System.out.println("Third---");
}
}
class Fourty extends Third{
public Fourty(){
System.out.println("Fourty---");
}
}
class Fifty extends Fourty{
public Fifty(){
System.out.println("Fifty---");
}
}
public class Sixty extends Fifty{
public Sixty(){
System.out.println("Sixty---");
}
First first = new First();
Second second = new Second();
public static void main(String[] args) {
new Sixty();
}
}

    类的加载过程是自上而下的,因此会先寻找基类进行加载,待所有继承类就加载完成之后,加载子类的成员变量以及构造函数。

png5

根据前面的例子,总结程序中各部分内容初始化的顺序如下:

1.初始化基类中用到的静态变量,静态方法;
2.初始化main()方法中的常量,如果是有静态变量的对象,先初始化静态变量,然后加载其构造器;
3.加载基类构造器;
4.按顺序初始化成员变量;
5.加载子类构造器。

四、协变返回类型

    JavaSE5中新增了协变返回类型,在子类中的覆盖方法,可以返回其基类中该方法返回类型的某一个子类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.chenxyt.java.practice;
class Father{
public String toString(){
return "Father";
};
}
class Son extends Father{
public String toString(){
return "Son";
}
}
class Mill{
Father process(){
return new Father();
}
}
class wheatMill extends Mill{
Son process(){
return new Son();
}
}
public class Test{
public static void main(String[] args) {
Mill m = new Mill();
Father f = m.process();
System.out.println(f);
m = new wheatMill();
f = new Son();
System.out.println(f);
}
}

运行结果:

png6

五、用继承进行设计

    从某些角度来看,继承会增加程序的复杂性,在程序设计过程中应优先使用组合。如果单纯的是想使用某一个类,让这个类的对象完成一些功能,那么使用组合会更好一些。此外, 对于继承中多态的实现,很大部分原因是由于向上转型与动态绑定,针对向上转型,与之相对的叫做向下转型,我们都知道向上转型是安全的,而由于扩展性的原因,向下转型并不是安全的。因为父类可能并没有子类中的一个方法。

六、总结

    多态是面向对象“封装”、“继承”、“多态”三大特性之一,理解多态的特性能够更好的设计程序,同时,掌握程序初始化加载的过程能够更好的理解程序,这也是重中之重。

1234

Crayon Cxy

Go over the mountain, and they will hear your story.

38 日志
3 分类
14 标签
友情链接
  • 六脉神间
  • My CSDN
© 2019 Crayon Cxy
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4