一、对象
1.1用引用操纵对象
每种编程语言都有自己操纵内存中元素的方式。你是直接操纵对象还是间接操纵呢?C和C++中,用指针来操纵对象。
尽管Java把一切都当成对象,但操纵的标识符实际上是对象的一个“引用”。可以把这样的情景假想成用“遥控器”(引用)来操纵“电视机”(对象)。
即使没有电视机,遥控器也可以独立存在。也就是说,你拥有一个引用,并不一定需要有一个对象与它相关联。因此,如果想操纵一个句子或词,可以创建一个String引用:String s; 注意,这里的s只是一个引用,并非对象。安全的做法就是在创建一个引用的同时对其进行初始化。String s=“abcde”; 或者String s=new String(“abcde”);前者首先在栈中创建一个引用变量s,然后查看栈中是否存在“abced”,如果没有,则将“abcde”存放进栈,并让引用变量s指向它;如果有,则直接让s指向它即可;后者是java中标准的对象创建方式,其创建的对象将直接放置到堆中,每调用一次就会创建一个新的对象。
1.2必须有你创建所有的对象
1.2.1存储到什么地方
程序运行时,对象怎么进行放置安排的呢?特别是内存如何分配的?有5个不同的地方可以存储数据:
1)寄存器。最快的存储区,位于处理器内部。数量有限,故它根据需求分配。
2)堆栈。位于RAM。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。创建程序时,系统必须知道存储在堆栈中的所有数据的生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性。
3)堆。一种通用内存池,用于存放所有java对象。相比堆栈,它编译器不需要知道存储的数据在堆里的生命周期。灵活性强。当需要一个对象时,只需用new写一行简单的代码,当执行这行代码时,胡自动在堆里进行存储分配。但为这种灵活性符出的代价是,用堆进行存储分配和清理可能比堆栈更费时。
4)常量存储。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会改变。
5)非RAM存储。如果数据完全存活于程序之外,则它可以不受程序任何控制,在程序没有运行时也可以存在。这种存储方式的技巧在于:把对象转换为可以存放在其他媒介上的事物,在需要时,恢复成常规的、基于RAM的对象。
1.2.2基本类型
在程序设计中常用的一系列类型,它们需要特殊对待。之所以特殊对待,是因为new将对象存储在“堆”里,用new创建一个对象——特别是小的、简单的变量,往往不是很有效。因此,对于这些类型,java采取的方法是,不是用new来创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
1.3永远不需要销毁对象
事实证明,由new创建的对象,只要你需要,就会一直保留下去。这样便带来一个有趣的问题,如果java让对象继续存在,那么靠什么才能防止这些对象填满内存空间,进而阻塞你的程序?
java有一个垃圾回收器,用来监视用new创建的所有对象,并辨别那些不会再被引用的对象。随后,释放这些对象的内存空间,以便供其他新的对象使用。这样做就消除了“内存泄漏”问题,这是由于程序员忘记释放内存而产生的问题。
1.6 static关键字
通常,你必须创建一个对象,并用它来访问数据或方法。但,当声明一个事物是static时,就意味着这个域或者方法不会与包含它的那个类的任何对象实例关联在一起。所以,即使从未创建某个类的任何对象,也可以调用其static方法或访问其static域。
引用static对象有两种。一种是通过一个对象去定位它,另一种是通过类名直接引用(这对于非静态成员则不行)。
前者例子:
class Test{
static int i=47;
}
Test t1=new Test();
Test t2=new Test();
- 1
- 2
- 3
- 4
- 5
t1.i和t2.i的值均为47。
后者例子:
Test.i++;
- 1
t1.i和t2.i的值均为48。
静态方法同理,可以通过对象调用,也可以直接用类名直接调用。
二、操作符
2.1优先级
2.2赋值
“将一个对象赋值给另一个对象”,实际是将“引用”从一个地方复制到另一个地方。这意味着若对对象使用c=d,那么c和d都指向原本只有d指向的那个对象。
三、控制执行流程
if-else
while
do-while
while和do-while的唯一区别在于do-while中的语句至少会执行一次。而在while中,若条件第一次就为false,则其中的语句一句都不执行。while比do-while常用。
for
switch
return
return有两方面用途,一个是指定一个方法返回什么值(假设它没有void返回值),另一个是它会导致当前方法的推出的退出,并返回那个值。
break
break用于强制退出循环,不执行循环中剩余的语句。
continue
continue则停止执行当前循环,然后退回循环起始处,开始下一次的迭代。
foreach语法
若想遍历某个int型数组arr中的每一个元素,一种方法是用i去遍历数组的下标实现,另一个方法是for(int i:arr){System.out.print(i+",");}.
四、初始化与清理
4.1用构造器确保初始化
1、构造器的命名与类名相同。
2、在创建对象时,将会为对象分配存储空间,并调用相应的构造器。这就确保在你能操作对象之前,完成对对象的初始化。
3、构造器分为无参(默认)构造器和含参构造器。
无参构造器:
class A{
A(){
System.out.println("Rock");
}
}
public class Test {
public static void main(String[] args) {
for(int i=0;i<10;i++) {
new A();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
输出:
Rock
Rock
Rock
Rock
Rock
Rock
Rock
Rock
Rock
Rock
含参构造器:
class A{
A(int i){
System.out.println("Rock"+i);
}
}
public class Test {
public static void main(String[] args) {
for(int i=0;i<10;i++) {
new A(i);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
输出:
Rock0
Rock1
Rock2
Rock3
Rock4
Rock5
Rock6
Rock7
Rock8
Rock9
4、构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可以选择它返回别的东西。构造器则不会返回任何东西,你别无选择。
4.2方法重载
1、构造器重载和方法重载的例子:
class Tree{
int height;
Tree(){
height=0;
System.out.println("Planting a tree seed.");
}
Tree(int initialH){
height=initialH;
System.out.println("Creating new Tree that is "+height+" feet tall.");
}
void info() {
System.out.println("This is "+height+" feet tall.");
}
void info(String s) {
System.out.println(s+":This is "+height+" feet tall.");
}
}
public class Test {
public static void main(String[] args) {
for(int i=0;i<5;i++) {
Tree t=new Tree(i);
t.info();
t.info("overload method");
}
new Tree();
}
}
- 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
输出:
Creating new Tree that is 0 feet tall.
This is 0 feet tall.
overload method:This is 0 feet tall.
Creating new Tree that is 1 feet tall.
This is 1 feet tall.
overload method:This is 1 feet tall.
Creating new Tree that is 2 feet tall.
This is 2 feet tall.
overload method:This is 2 feet tall.
Creating new Tree that is 3 feet tall.
This is 3 feet tall.
overload method:This is 3 feet tall.
Creating new Tree that is 4 feet tall.
This is 4 feet tall.
overload method:This is 4 feet tall.
Planting a tree seed.
2、区分重载方法
(1)每个重载方法都必须有一个独一无二的参数类型列表。可以用参数类型和参数列表的顺序加以区分。
(2)除此之外还可以用返回值来区分重载方法。
3、默认构造器
如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
class Tree{
}
public class Test {
public static void main(String[] args) {
Tree t=new Tree();/标注行
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
标注行创建了一个对象,并调用了其默认构造器——即使你没有明确去定义它。但是如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
class Tree{
Tree(int i){}
Tree(double d){}
}
public class Test {
public static void main(String[] args) {
Tree t1=new Tree();/标注行
Tree t2=new Tree(1);
Tree t3=new Tree(1.0);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
要是如标注行那样写,编译器就会报错:没有找到匹配的构造器。这就好比,要是你没有提供任何构造器,编译器会认为“你需要一个构造器,让我给你制造一个吧”。但是如果你已经写了一个构造器了,编译器会认为“啊,你已经写了一个构造器,所以你知道你在做什么。你是刻意省略了默认构造器。”
4、this关键字
假设你希望在方法内部获得对当前对象的引用。有一个专门的关键字:this。this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象的引用并无不同。但要注意,若在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。
只有当明确需要指出对当前对象的引用时,才需要使用this关键字。
5、构造器初始化
可以用构造器来进行初始化。但要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。
public class Test{
int i;
Test(){i=7;}
}
- 1
- 2
- 3
- 4
上述代码中,i首先会被初始化为0,然后再被初始化为7。
1)初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。
class Window{
Window(int i){System.out.println("Window("+i+")");}
}
class House {
Window w1=new Window(1);//构造器之前
House(){
System.out.println("House()");
w3=new Window(33);//w3在构造器内再次被初始化
}
Window w2=new Window(2);//构造器之后
void f() {System.out.println("f()");}
Window w3=new Window(3);//最后
}
public class Test7{
public static void main(String[] args) {
House h=new House();
h.f();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
输出:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
在House类中,故意把Window对象的定义散布到各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3在构造器内再次被初始化。
2)静态数据初始化
要想了解静态存储区域是何时初始化的,看下列例子:
class Bowl{
Bowl(int i){
System.out.println("Bowl("+i+")");
}
void f1(int i) {System.out.println("f1("+i+")");}
}
class Table{
static Bowl bowl1=new Bowl(1);//静态数据定义
Table(){
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int i) {System.out.println("f2("+i+")");}
static Bowl bowl2=new Bowl(2);//静态数据定义
}
class CupBoard{
Bowl bowl3=new Bowl(3);//非静态数据定义
static Bowl bowl4=new Bowl(4);//静态数据定义
CupBoard(){
System.out.println("CupBoard");
bowl4.f1(2);
}
void f3(int i) {System.out.println("f3("+i+")");}
static Bowl bowl5=new Bowl(5);//静态数据定义
}
public class Test7{
public static void main(String[] args) {
System.out.println("Creating new CupBorad() in main");
new CupBoard();
System.out.println("Creating new CupBorad() in main");
new CupBoard();
table.f2(1);
cupboard.f3(1);
}
static Table table=new Table();//静态对象定义
static CupBoard cupboard=new CupBoard();//静态对象定义
}
- 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
输出:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
CupBoard
f1(2)
Creating new CupBorad() in main
Bowl(3)
CupBoard
f1(2)
Creating new CupBorad() in main
Bowl(3)
CupBoard
f1(2)
f2(1)
f3(1)
由输出可见,静态初始化只有在必要时刻才会进行。如果不创建Table对象,那么静态的bowl1和bowl2永远不会被创建。只有在第一个Table对象被创建的时候,它们才会被初始化。此后,静态对象不会再次被初始化。但是,像在CupBoard类中的bowl3是非静态的,因此,每创建一个CupBoard对象时,bowl3就会被初始化一次。
初始化的顺序是,先静态对象,而后是“非静态对象”。要执行main()(静态方法),必须加载StaticInitialization类,然后其静态域table和cupboard被初始化,这将导致它们对应的类也被加载,并且由于它们都包含静态的Bowl对象,因此Bowl随后也被加载。这样,在这个特殊的程序中的所有类在main()之前就都被加载了。
3)显示的静态初始化
class Cup{
Cup(int i){System.out.println("Cup("+i+")");}
void f(int i) {System.out.println("f("+i+")");}
}
class Cups{
static Cup cup1;
static Cup cup2;
static {
cup1=new Cup(1);
cup2=new Cup(2);
}
Cups(){System.out.println("Cups()");}
}
public class Test7{
public static void main(String[] args) {
System.out.println("inside main()");
Cups.cup1.f(99);//(1)
}
//static Cups cups1=new Cups();//(2)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
输出:
inside main()
Cup(1)
Cup(2)
f(99)
无论是通过标(1)的那行代码访问静态的cup1对象,还是把标(1)的那行注释掉,运行标(2)的那行代码,Cups的静态初始化动作都会得到执行。如果把标(1)和(2)的行同时注释掉,Cups的静态初始化动作就不会执行。
4)非静态实例初始化
class Mug{
Mug(int i){System.out.println("Mug("+i+")");}
void f(int i) {System.out.println("f("+i+")");}
}
class Mugs{
Mug mug1;
Mug mug2;
{
mug1=new Mug(1);
mug2=new Mug(2);
System.out.println("mug1 and mug2 initialized.");
}
Mugs(){System.out.println("Mugs()");}
Mugs(int i){System.out.println("Mugs(int)");}
public static void main(String[] args) {
System.out.println("inside main()");
new Mugs();
System.out.println("new Mugs() completed.");
new Mugs(1);
System.out.println("new Mugs(1) completed.");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
输出:
inside main()
Mug(1)
Mug(2)
mug1 and mug2 initialized.
Mugs()
new Mugs() completed.
Mug(1)
Mug(2)
mug1 and mug2 initialized.
Mugs(int)
new Mugs(1) completed.
#####你可以看到实例初始化子句
{
mug1=new Mug(1);
mug2=new Mug(2);
System.out.println("mug1 and mug2 initialized.");
}
- 1
- 2
- 3
- 4
- 5
看起来它与静态初始化子句一模一样,只不过少了static关键字。它使得你可以保证无论调用了哪个显式构造器,某些操作都会发生。从输出中可以看到实例初始化子句是在两个构造器之前执行的。
五、访问权限控制
1、java访问权限修饰词
(1)包访问权限
默认访问权限没有任何关键字,但通常是指包访问权限(有时也表示成friendly)。这就意味着当前包中的所有其它类对那个成员都有访问权限,但对于这个包之外的所有类,这个成员却是private。
(2)public:接口访问权限
使用关键字public,就意味着public之后紧跟着的成员声明自己对每个人都是可用的。
(3)private:你无法访问
关键字private的意思是,除了包含该成员的类之外,任何其他类都无法访问这个成员。由于处于同一个包内的其他类是不可以访问private成员的,因此这等于说是自己隔离了自己。从另一个方面来说,就表示private就允许你随意改变该成员,而不必考虑这样做是否会影响到包内其他的类。
(4)protected:继承访问权限
关键字protected处理的是继承的概念,通过继承可以利用一个现有类——我们称其为基类,然后将新成员添加到该现有类中而不必碰该现有类。还可以改变该类的现有成员的行为。通过protected声明的成员,对于它所在的类、它的子类以及同一个包中的其他类来说是public的,而对于其他包来说它是private的。
六、复用类
1、组合和继承的区别
组合的优点
- 不破坏封装,整体类与局部类之间松耦合,彼此相对独立
- 具有较好的可扩展性
- 支持动态组合。在运行时,整体对象可以选择不同类型的局部对象
- 整体类可以对局部类进行包装,封装局部类的接口,提供新的接口
- 1
- 2
- 3
- 4
组合的缺点
- 整体类不能自动获得和局部类同样的接口
- 创建整体类的对象时,需要创建所有局部类的对象
- 1
- 2
继承的优点
- 子类能自动继承父类的接口
- 创建子类的对象时,无须创建父类的对象
- 1
- 2
继承的缺点
- 破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
- 支持扩展,但是往往以增加系统结构的复杂度为代价
- 不支持动态继承。在运行时,子类无法选择不同的父类
- 子类不能改变父类的接口
- 1
- 2
- 3
- 4
2、向上转型
class Instrument{
public void play() {}
static void tune(Instrument i) {
//...
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind w=new Wind();
Instrument.tune(w);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在上例中,tune()方法可以接受Instrument引用。但在Wind.main()中,传递给tune()方法的是一个Wind引用。在tune()中,程序代码可以对Instrument和它所有的导出类(子类)起作用,这种将Wind引用转换为Instrument引用的动作,我们称之为向上转型。
3、final关键字
(1)final数据
许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:
1、一个永不改变的编译时常量。
2、一个在运行时被初始化的值,而你不希望它被改变。
- 1
- 2
对于编译时常量这种情况,编译器可以将常量值带入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这就减轻了一些运行时的负担。在Java中,这类常量必须是基本数据类型,而且以关键字final表示。在对这个常量进行定义的时候,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。
对于基本类型,final使数值恒定不变。
而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为只想另一个对象。然而,对象其自身却是可以修改的。
(2)final方法
使用final方法的原因有两个。第一个是把方法锁定,以防任何继承类修改它的含义。想要确保在继承中使方法行为保持不变,而且不会被覆盖。另一个原因是效率。
#####(3)final类
当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或出于安全考虑,你不希望它有子类。由于final类禁止继承,所以final类中所有方法都是隐式指定为final的,因为无法覆盖它们。在final类中可以给方法添加修饰词,但这毫无意义。
七、多态
1、多态
多态,说白了就是不同对象针对于同一个事件做出的不同的响应。
class Meal{
public void play() {System.out.println("Meal.play");}
}
class Lunch extends Meal{
public void play() {System.out.println(“Lunch.play”);}
}
class PortableLunch extends Lunch{
public void play() {System.out.println(“PortableLunch.play”);}
}
public class SandWich extends PortableLunch{
public void play() {System.out.println(“SandWich.play”);}
public static void main(String [] args) {
Meal meal=new Meal();
Meal meal2=new Lunch();/
Meal meal3=new PortableLunch();//
Meal meal4=new SandWich();/
meal.play();
meal2.play();
meal3.play();
meal4.play();
}
}
- 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
输出:
Meal.play
Lunch.play
PortableLunch.play
SandWich.play
上个例子中,meal、meal2、meal3、meal4四个不同的对象针对play这同一个事件,做出了不同的响应。在标注行,你可能会注意到,在这里,创建了Lunch、PortableLunch、SandWich对象并把得到的引用赋值给Meal,这样看似错误。但实际上没问题,因为通过继承,Lunch、PortableLunch、SandWich就是一种Meal。
2、构造器和多态
class Meal{
Meal(){System.out.println("Meal()");}
}
class Bread{
Bread(){System.out.println("Bread()");}
}
class Cheese{
Cheese(){System.out.println("Cheese()");}
}
class Lettuce {
Lettuce(){System.out.println("Lettuce()");}
}
class Lunch extends Meal{
Lunch(){System.out.println("Lunch()");}
}
class PortableLunch extends Lunch{
PortableLunch(){System.out.println("PortableLunch()");}
}
public class SandWich extends PortableLunch{
private Bread b=new Bread();
private Cheese c=new Cheese();
private Lettuce l=new Lettuce();
public SandWich() {System.out.println("SandWich()");}
public static void main(String [] args) {
new SandWich();
}
}
- 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
输出:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
SandWich()
在上例中,用其它类创建了一个复杂的类,而且每个类都有一个声明它自己的构造器。其中最重要的类是SandWich,它反映了三层继承。当在main里创建一个SandWich对象后,就可以看到输出结果。这也表明了这一复杂对象调用构造器要遵循下面的顺序:
1)调用基类构造器。这个步骤不断反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,知道最低层的导出类。
2)按声明顺序调用成员的初始化方法。
3)调用导出类构造器的主体。
- 1
- 2
- 3
八、接口
1、抽象类和抽象方法
抽象方法:这种方法是不完整的,仅有声明而没有方法体。
abstract void f();
- 1
抽象类:含有抽象方法的类叫做抽象类。如果一个类含有一个或者多个抽象方法,该类必须被限定为抽象的。(否则便一起会报错。)
如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,而且编译器将会强制我们也abstract关键字来限定这个类。
2、接口
interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现。它允许创建者提供方法名、参数列表和返回类型,但是没有任何方法体。
接口只提供了形式,而未提供任何具体实现。
interface不仅仅是一个极度抽象的类。
想要创建一个接口,需要用interface关键字来代替class关键字。就像类一样,可以在interface关键字前面添加public关键字(仅限于该接口在与其同名的文件中被定义)。如果不添加public,则它只具有包访问权限,这样它就只能在同一个包内可用。接口也可以包含域,但这些域隐式地是static和final的。
要让一个类实现某个特定接口,需要使用implements关键字。
注意:一旦实现了某个接口,其实现就变成了一个普通的类,就可以按常规方式去扩展它。
在接口中的每一个方法确实都只是一个声明,这是编译器所允许的在接口中唯一能够存在的事物。此外,在接口中没有任何方法被声明为是public,但是它们自动就都是public的。
3、Java中的多重继承
java中有多重继承的概念,但却没有多继承的概念,一个类有且仅有一个父类,这是java单继承的局限性。java中通过实现接口来达到多继承的功能目的。一个类只能继承一个类,但是却可以实现多个接口!例如:
interface CanFight { void fight(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } public class ActionCharacter { public void fight(){
}
}
public class Hero extends ActionCharacter implements CanFight,CanFly,CanSwim{
public void fly() {
}
public void swim() {
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
可以看到,Hero组合了具体类ActionCharacter和接口CanFight、CanFly、CanSwim。当通过这种方式将一个具体类和多个接口组合到一起的时候,这个具体类必须放在前面,后面跟着的才是接口(否则编译器会报错)。
4、通过继承来扩展接口
interface Monster{
void menace();
}
interface DangerousMonster extends Monster{
void destroy();
}
interface Lethal{
void kill();
}
class DragonZilla implements DangerousMonster{
public void menace(){}
public void destroy() {}
}
interface Vampire extends DangerousMonster,Lethal{
void drinkBllod();
}
class VeryBadVampire implements Vampire{
public void destroy() { }
public void menace() {}
public void kill() {}
public void drinkBllod() {}
}
public class InterfaceExtends {
static void u(Monster b){
b.menace();
}
static void v(DangerousMonster d){
d.menace();
d.destroy();
}
static void w(Lethal l){
l.kill();
}
public static void main(String[] args){
DangerousMonster barney = new DragonZilla();
u(barney);
v(barney);
Vampire vlad = new VeryBadVampire();
u(vlad);
v(vlad);
w(vlad);
}
}
- 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
DangerousMonster 是Monster 的直接扩展,它产生了一个新接口。DragonZilla实现了这个接口。
在Vampire中使用的语法仅适用于接口继承。一般情况下,只可以将extends用于单一类,但可以引用多个基类接口。
九、内部类
1、创建内部类
public class Test5 {
class Contents{
private int i=11;
public int value() {return i;}
}
class Destination{
private String s;
Destination(String s2){
s=s2;
}
String read() {return s;}
}
public void ship(String ss) {
Contents c=new Contents();
Destination d=new Destination(ss);
System.out.println(d.read());
}
public static void main(String[] args){
Test5 t=new Test5();
t.ship("Hello.");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
当我们在ship()方法里使用内部类的时候,与使用普通类没什么不同。在这里,实际的区别的就是内部类的名字是嵌套在Test5里面的。
更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在to()和contents()方法中看到的那样:
public class Test5 {
class Contents{
private int i=11;
public int value() {return i;}
}
class Destination{
private String s;
Destination(String s2){
s=s2;
}
String read() {return s;}
}
public Contents contents() {
return new Contents();
}
public Destination to(String s) {
return new Destination(s);
}
public void ship(String ss) {
Contents c=new Contents();
Destination d=new Destination(ss);
System.out.println(d.read());
}
public static void main(String[] args){
Test5 p=new Test5();
p.ship("Hello.");
Test5 q=new Test5();
Test5.Contents c=q.contents();
Test5.Destination d=q.to("World.");
}
}
- 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
如果想从外部类的非静态方法之外的任何位置创建某个内部类的对象,那么必须想在main()方法中那样,具体地指明这个对象的类型:OuterClassName.InnerClassName。
2、链接到外部类
在JAVA中,内部类似乎是一种名字隐藏和组织代码的一种形式,但内部类还有另一个最重要的用途:当生成一个内部类的对象时,此对象与制造它的外围对象之间构建了一种特殊的联系,内部类对象能够访问外围类对象的所有成员,而不需要任何特殊条件。
3、使用.this与.new
如果需要生成外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。这样产生的引用自动具有正确类型。
使用.this的例子:
public class DoThis {
void f() {System.out.println("DoThis.f()");}
public class Inner{
public DoThis outer() {
return DoThis.this;
}
}
public Inner inner() {return new Inner();}
public static void main(String[] args){
DoThis dt=new DoThis();
DoThis.Inner dti=dt.inner();
dti.outer().f();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
输出:DoThis.f()
使用.new的例子:
public class DoThis {
public class Inner{
Inner(){System.out.println("Inner.Constructor");}
}
public static void main(String[] args){
DoThis dt=new DoThis();
DoThis.Inner dti=dt.new Inner();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
输出:Inner.Constructor
十、持有对象
通常,程序总是根据运行时才知道的某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型。为解决这个问题,就需要在任意时刻任意位置创建任意数量的对象。所以就不能靠创建命名的引用来持有每一个对象。大多数语言提供某种方法来解决这个基本问题。Java有多种方法保存对象。数组是保存一组对象最有效的方式,但是数组的长度是受到限制的。Java实用类库还提供一套相当完整的容器来解决这个问题,其中基本类型由List、Set、Queue和Map。
1、泛型和类型安全的容器
SE5之前的容器的一个主要问题是编译器允许你向容器插入不正确的类型。我们使用最基本可靠的容器ArrayList,你可以用add()插入对象,然后可以用get()访问这些对象,此时需要使用索引,像数组一样,但不需要中括号。ArrayList还有一个size()方法,使你可以知道添加进来了多少个元素,从而不会因索引越界而引发错误。
例子1:
import java.util.ArrayList;
class Apple{
private static long counter;
private final long id=counter++;
public long id() {return id;}
}
class Pear{}
public class Test {
public static void main(String[] args) {
ArrayList apples=new ArrayList();
for(int i=0;i<3;i++) {
apples.add(new Apple());
}
apples.add(new Pear());
for(int i=0;i<apples.size();i++) {
((Apple)apples.get(i)).id();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
上例中,Apple和Pear类是有区别的,它们除了都是Object之外没有任何共性。因为ArrayList保存的是Object,因此你不仅可以通过add()方法将Apple对象放进容器,还可以添加Pear对象,无论在编译时还是运行时都没有问题。
但是当你用get()方法取出你认为是Apple的对象时,你得到的只是Object引用,必须将其转型为Apple,因此需要将整个表达式括起来,在调用id()方法之前,强制执行转型。
在运行时,当你试图将Orange对象转型为Apple时,就会出错。
使用Java泛型来创建类会非常复杂,但是应用预定义的泛型通常会很简单。例如,要想定义用来保存Apple对象的ArrayList,可以声明ArrayList< Apple>,而不仅仅只是ArrayList,其中尖括号括起来的时类型参数(可以有多个),他指定了这个容器实例可以保存的类型。通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。
例子2:
import java.util.ArrayList;
class Apple{
private static long counter;
private final long id=counter++;
public long id() {return id;}
}
class Pear{}
public class Test {
public static void main(String[] args) {
ArrayList<Apple> apples=new ArrayList<Apple>();
for(int i=0;i<3;i++) {
apples.add(new Apple());
}
//apples.add(new Pear()); /这里编译时不会通过
for(int i=0;i<apples.size();i++) {
((Apple)apples.get(i)).id();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
上例中,编译器可以阻止你将Pear放置到容器中,因此它变成了一个编译期错误,而不再是运行时错误。
2、基本概念
Java容器类类库用途时“保存对象”,并将其划分成两个不同的概念:
#####(1)Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入顺序保存元素。而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序。
#####(2)Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数字对象来查找某个对象,它也被称为“关联数组”,因为它将某些对象与另外一些对象关联在一起;或者被称为“字典”,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。
在理想的情况下,你编写的大部分代码都是在与这些接口打交道,并且你唯一需要指定所使用的精确类型就是在创建的时候。因此,你可以像下面一样创建一个List:
List<Apple> apples=new ArrayList<Apple>();
- 1
注意,ArrayList已经向上转型为List。使用接口的目的在于如果你决定去修改你的实现,你所需的只是在创建时修改它,像这样:
List<Apple> apples=new LinkedList<Apple>();
- 1
这种方法并不是所有情况下都奏效的,因为某些类有额外的功能。例如,LinkedList具有List接口中没有的额外的方法,而TreeMap也有在Map接口中没有的方法。如果你需要使用这些方法,就不能将它们向上转型为更通用的接口了。
例子:
import java.util.Collection;
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
Collection<Integer> c=new LinkedList<Integer>();
for(int i=0;i<9;i++) {
c.add(i);
}
for(Integer i:c) {
System.out.print(i+" ");
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
输出:0 1 2 3 4 5 6 7 8
3、添加一组元素
在java.util包中的Arrays和Collections类中都有很多实用方法,可以在一个Collection中添加一组元素。
Arrays.asList()方法接受一个数组或用逗号隔开的元素列表,并将其转换为一个List对象。Collections.addAll()方法接受一个Collection对象,以及一个数组或是用一个逗号分隔的列表,将元素添加到Collection中。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
public class Test {
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,15);
Collections.addAll(collection, moreInts);
for(Integer i:collection) {
System.out.print(i+" ");
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
输出:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 6 7 8 9 10
Collection.addAll()成员方法只能接受另一个Collection对象作为参数,因此它不如Arrays.asList()或Collcetions.addAll()灵活,这两个方法都是可变参数列表。
4、List
List接口在Collection接口的基础上添加了大量的方法,使得可以在List的中间插入和删除元素。
有两种类型的List:
#####1、基本的ArrayList,它优于随机访问元素,但是在List的中间插入和删除操作元素时较慢,因为ArrayList要移动数据。
#####2、LinkedList,它增加和删除元素相对较快,但是随机访问需要很长时间,因为需要移动指针。
5、迭代器
如何不重写代码就可以应用于不同类型的容器?
迭代器(也是一种设计模式)的该奶奶可以用于达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象。它通常被称为:轻量级对象,因为创建它的代价小。因此,经常可以见到对迭代器有些奇怪的限制,例如,Java的Iterator只能单向移动,这个Iterator只能用来:
1)使用方法iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
2)使用next()获得序列中的下一个元素。
3)使用hasNext()检查序列中是否还有元素。
4)使用remove()将迭代器新近返回的元素删除。
- 1
- 2
- 3
- 4
例子:
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new LinkedList<String>();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String string = (String)iterator.next();
System.out.println(string);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
输出:
111
222
333
444
6、Stack
“栈”通常指“后进先出”的容器。
例子:
import java.util.Stack;
public class Test {
public static void main(String[] args) {
Stack<String> stack=new Stack<String>();
for(String s:"I am a good girl".split(" ")) {
stack.push(s);
}
while(!stack.isEmpty()) {
System.out.print(stack.pop()+" ");
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
输出:girl good a am I
7、Set
Set不保存重复的元素。
例子1:
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class Test {
public static void main(String[] args) {
Random r=new Random(47);
Set<Integer>set=new HashSet<Integer>();
for(int i=0;i<10000;i++) {
set.add(r.nextInt(10));
}
System.out.println(set);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
输出:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
在上例中,在0~29之间的10000个随机数被添加到Set中,因此你可以想象,每一个数都被重复了许多次。但是你可以看到,每一个数只有一个实例出现在结果中。
例子2:
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Test {
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".split(" "));
set1.add("M");
System.out.println("set1有D吗:"+set1.contains("D"));
System.out.println("set1有Y吗:"+set1.contains("Y"));
Set<String> set2=new HashSet<String>();
Collections.addAll(set2, "H I J K".split(" "));
System.out.println("set2在set1里吗:"+set1.containsAll(set2));
set1.remove("G");
System.out.println("删除G后的set1:"+set1);
set1.removeAll(set2);
System.out.println("从中删除set2后的set1:"+set1);
Collections.addAll(set1, "X Y Z".split(" "));
System.out.println("添加XYZ后的set1:"+set1);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
输出:
set1有D吗:true
set1有Y吗:false
set2在set1里吗:true
删除G后的set1:[A, B, C, D, E, F, H, I, J, K, M]
从中删除set2后的set1:[A, B, C, D, E, F, M]
添加XYZ后的set1:[A, B, C, D, E, F, X, Y, Z, M]
8、Map
可以通过使用containsKey()和containsValue()的方法查看map中是否包含某个键或者某个值。
import java.util.HashMap;
import java.util.Map;
public class Test8 {
public static void main(String[] args) {
Map<String,Integer>map=new HashMap<String,Integer>();
map.put("A", 95);
map.put("B", 85);
map.put("C", 75);
map.put("D", 65);
System.out.println(map);
System.out.println("map包含C吗:"+map.containsKey("C"));
System.out.println("map包含95吗:"+map.containsValue(95));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
输出:
{A=95, B=85, C=75, D=65}
map包含C吗:true
map包含95吗:true
9、Queue
队列是典型的先进先出的容器。
LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。通过将LinkedList向上转型为Queue。
例子:
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
public class Test {
public static void printQ(Queue q) {
while(q.peek()!=null)
System.out.print(q.remove()+" ");
System.out.println();
}
public static void main(String[] args) {
Queue<Integer> q=new LinkedList<Integer>();
Random r=new Random(47);
for(int i=0;i<10;i++) {
q.offer(r.nextInt(i+7));
}
System.out.println(q);
Queue<Character>qc=new LinkedList<Character>();
for(char c:“helloworld”.toCharArray()) {
qc.offer©;
}
System.out.println(“删除队列之前的qc:”+qc);
printQ(qc);
System.out.println(“删除队列之后的qc:”+qc);
}
}
- 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
输出:
[6, 3, 4, 1, 0, 5, 11, 12, 12, 11]
删除队列之前的qc:[h, e, l, l, o, w, o, r, l, d]
h e l l o w o r l d
删除队列之后的qc:[]
10、总结:
java提供了大量持有对象的方式:
1)数组将数字和对象联系起来。他保存类型明确的对象,查询对象时,不需要对结果做类型转换。可以是多维的,可以保存基本类型的数据。但是,一旦数组生成,其容量就固定不变了。
2)Collection保存单一元素,Map保存相关联的键值对。有了Java的泛型,你就可以指定容器中存放的对象类型,且从容器中获得元素时,不必进行类型转换。各种Collection和Map都可以在你向其中添加更多元素时,自动调整尺寸。
3)像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排好序的容器。List能自动扩容。
4)如果要进行大量随机访问,就使用ArrayList;如果要经常从表中增删元素,就应该用LinkedList。
5)各种Queue和栈的行为,有LinkedList提供支持。
6)Map是一种将对象(而非数字)与对象相关联的设计。 HashMap设计用来快速访问;而TreeMap保持“键”始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入的顺序,但也通过散列提供了快速访问的能力。
7)Set不接受重复的元素。HashSet提供了最快查询速度,而TreeSet保持元素处于排序状态。LinkedHashSet以插入顺序保存元素。
十一、通过异常处理错误
Java异常机制用到的几个关键字:try、catch、finally、throw、throws。
• try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
• catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
• finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
• throw – 用于抛出异常。
• throws – 用在方法签名中,用于声明该方法可能抛出的异常
这里针对finally在return之前执行还是之后执行做一个拓展总结:
情况1:
try{
///
}catch(){
///
}finally{
///
}
return;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
顺序执行。
情况2:
try{
return;
}catch(){
///
}finally{
///
}
return;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
程序执行try块中return之前(包括return语句中的表达式运算)代码;再执行finally块,最后执行try中return;finally块之后的语句return,因为程序在try中已经return所以不再执行。
情况3:
try{
///
} catch(){
return;
} finally{
///
}
return;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
程序先执行try,如果遇到异常执行catch块,有异常:则执行catch中return之前(包括return语句中的表达式运算)代码,再执行finally语句中全部代码,最后执行catch块中return. finally之后也就是4处的代码不再执行。无异常:执行完try再finally再return.
情况4:
try{
return;
}catch(){
///
} finally{
return;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
程序执行try块中return之前(包括return语句中的表达式运算)代码;再执行finally块,因为finally块中有return所以提前退出。
情况5 :
try{
///
} catch(){
return;
}finally{
return;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
程序执行catch块中return之前(包括return语句中的表达式运算)代码;再执行finally块,因为finally块中有return所以提前退出。
情况6:
try{
return;
}catch(){
return;
} finally{
return;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
程序执行try块中return之前(包括return语句中的表达式运算)代码;有异常:执行catch块中return之前(包括return语句中的表达式运算)代码; 则再执行finally块,因为finally块中有return所以提前退出。无异常:则再执行finally块,因为finally块中有return所以提前退出。
1、基本异常
普通问题和异常情形是有区别的。
普通问题:指当前环境下能得到足够信息,总能处理这个错误。
异常情形:不能继续下去,因为当前环境下无法获得必要信息来解决问题。就需要从当前环境跳出,把问题提交给上一级环境。
当异常抛出后,有几件事会随之发生。首先,同java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路线被终止,而且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式执行,要么继续运行。
异常参数:与java中其他对象一样,我们总是用new在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器:一个是默认构造器,一个是接受字符串作为参数,以便将相关信息放入异常对象的构造器:throw new NullPointerException(“t=null”);
2、捕获异常
要明白异常时如何被捕获的,必须首先理解监控区域的概念。它是一段可能产生异常的代码,而且后面跟着处理这些异常的代码。
(1)try块
如果在方法内部抛出了异常,这个方法将在抛出异常的过程中结束。要是不希望该方法就此结束,可以在方法内部设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种(可能产生异常的)方法调用,所以成为try块。它是跟在关键字try后面的普通程序块:
try{
//这里可能产生异常
}
- 1
- 2
- 3
(2)异常处理程序
抛出的异常必须要在某个地方得到处理。这个“地点”就是异常处理程序,而且针对每一个要捕获的异常,得准备响应的处理程序。异常处理程序紧跟在try块后面,以关键字catch表示:
try{
//这里可能产生异常
}catch(Type1 id1){
//这里解决类型1的异常
}catch(Type2 id2){
//这里解决类型2的异常
}catch(Type3 id3){
//这里解决类型3的异常
}
//等等...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
3、创建自定义异常
java提供的异常体系不可能预见所有的希望加以报告的错误,所以可以自己定义异常来表示程序中可能会遇到的特定问题。
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常不容易找)。建立新的异常类的最简单的方法就是让编译器为你产生默认构造器,所以这几乎不需要写多少代码:
class SimpleException extends Exception{} public class InheritingExceptions{
public void f() throws SimpleException{ System.out.println("Throw SimpleException from f()."); throw new SimpleException(); } public static void main(String[] args) { InheritingExceptions sed=new InheritingExceptions(); try { sed.f(); }catch(SimpleException e) { System.out.println("Caught it!"); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
输出:
Throw SimpleException from f().
Caught it!
在上例中,编译器创建了默认构造器,它将自动调用基类默认构造器。
(1)异常和记录日志
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
class LoggingException extends Exception{
private static Logger logger=Logger.getLogger(“LoggingException”);
public LoggingException() {
StringWriter trace=new StringWriter();
printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
}
public class LoggingExceptions{
public static void main(String[] args) {
try {
throw new LoggingException();
}catch(LoggingException e) {
System.err.println("Caught "+e);
}
try {
throw new LoggingException();
}catch(LoggingException e) {
System.err.println("Caught "+e);
}
}
}
- 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
输出:
九月 14, 2018 4:18:52 下午 LoggingException
严重: LoggingException
at LoggingExceptions.main(LoggingExceptions.java:16)
Caught LoggingException
九月 14, 2018 4:18:52 下午 LoggingException
严重: LoggingException
at LoggingExceptions.main(LoggingExceptions.java:21)
Caught LoggingException
在上例中,静态的Logger.getLogger()方法创建了一个String参数相关联的Logger对象,这个Logger对象会将其输出发送到System.err。向Logger写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是severe()。为了产生日志记录消息,我们欲获取异常抛出处的轨迹栈,但是printStackTrace()不会默认地产生字符串。为了获取字符串,我们需要使用重载的printStackTrace()方法,它接受一个java.io.StringWriter对象作为参数。如果我们将一个java.io.StringWriter对象传递给这个printWriter的构造器,那么通过调用toString
()方法,就可以将输出抽取为一个String。
4、java标准异常
Throwable这个java类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型。
(1)Error用来表示编译时和系统错误(除特殊情况外,一般不用你关心)。
(2)Exception是可以被抛出的的基本类型,在java类库、用户方法以及运行时故障中都有可能抛出Exception型异常。所以java程序员关心的基本类型通常是Exception。
5、使用finally进行清理
对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况(因为回收由垃圾回收器完成)。为了达到这个效果,可以在异常处理程序后面加上finally子句。
try{
//这里是监控区域
//这里可能抛出A、B、C类型的异常
}catch(A a){
//这里解决A类型的异常
}catch(B b){
//这里解决B类型的异常
}catch(C c){
//这里解决C类型的异常
}finally{
//无论是否抛出异常,这里总能执行
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
6、构造器
如果异常发生了,大多数情况下,所有的东西都能被正确清理。但是,涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕且用户调用了特殊的清理方法之后才能得到清理。如果在构造器内抛出了异常,这些清理行为也许就不能正常工作了。这意味着在编写构造器的时候要格外小心。
7、异常匹配
抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序后,它就认为异常将得到处理,然后就不再继续查找。
查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。
8、总结
异常是java程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的事情,而在另一头处理你编写的这段代码中产生的错误。
十二、字符串
1、不可变String
String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上是JVM又创建了一个新的同名对象,然后再把原对象的值做修改后再赋值给新的对象,而原来的对象就会被JVM的垃圾回收机制(GC)给回收掉了,所以,原String对象实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。
例子:
public class Test {
public static void main(String[] args) {
String str="abc";
System.out.println(str);
str+="de";
System.out.println(str);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
输出:
abc
abcde
上例中,首先创建一个String对象str,并把“abc”赋值给str,然后JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程。
例子:
public class Test {
private static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String s="helloworld";
System.out.println(s);
String s2=upcase(s);
System.out.println(s2);
System.out.println(s);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
输出:
helloworld
HELLOWORLD
helloworld
上例中,当把s传给upcase()方法时,实际传递的是引用的一个拷贝。其实,每当把String对象作为方法的参数时,都会复制一份引用,而该引用所指的对象一直待在单一的物理位置上,从未动过。
2、String上的操作
-构造器:创建String对象
-length():String中字符的个数
-charAt():取得String中该索引位置上的char
-getChars()、getBytes():复制char或byte到一个目标数组中
-toCharArray():生成一个char数组,包含String的所有字符
-equals():比较两个String的内容是否相同
-compareTo():按词典序比较String内容,比较结果为负数、零或正数。注意:大小写不等价
-contains():如果该String对象包含参数的内容,则返回true
-contentEquals():如果该String与参数的内容完全一致,则返回true
-equalsIgnoreCase():忽略大小写,如果两个String内容相同,则返回true
-regionMatcher():返回boolean结果,以表明所比较区域是否相等
-startsWith():返回boolean结果,以表明该String是否以此参数起始
-endsWith():返回boolean结果,以表明此参数是否是该String的后缀
-indexOf():如果该String并不包含此函数,就返回-1;否则返回此参数在String中的起始索引
-lastIndexOf():是从后向前搜索
-subString():返回一个新的String,以包含参数指定的子字符串
-concat():返回一个新的String对象,内容为原始String连接上参数String
-replace():返回替换字符后的新String对象。如果没有发生替换,则返回原始String对象
-toLowerCase():将字符大小写改变后返回一个新String对象。若没改变发生则返回原始String对象
-toUpperCase():将字符大小写改变后返回一个新String对象。若没改变发生则返回原始String对象
-trim():将String两端的空白字符删除后返回一个新String对象。若没改变发生则返回原始String对象
-valueOf():返回一个表示参数内容的String
-intern():为每个唯一的字符序列生成一个且仅生成一个String引用
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
3、格式化输出
(1)printf
printf适用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号隔开,排成一行。
例子:
public class Test {
public static void main(String[] args) {
int x=5;
float y=7.0f;
System.out.printf("[%d,%.2f]\n",x,y);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
输出:[5,7.00]
(2)System.out.format()
该方法模仿printf(),如果你比较怀旧的话,也可以使用printf()。
例子:
public class Test {
public static void main(String[] args) {
int x=5;
double y=7.123456;
//老方法
System.out.println("["+x+","+y+"]");
//printf用法
System.out.printf("[%d,%f]\n",x,y);
//format用法
System.out.format("[%d,%f]\n",x,y);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
输出:
[5,7.123456]
[5,7.123456]
[5,7.123456]
上例可以看出,format()和printf()是等价的,它们只需要一个简单的格式化字符串,加上一串参数即可,每个参数对应一个格式修饰符。
4、正则表达式
(1)基础
一般来说,正则表达式就是以某种方式来描述字符串。例如,要找一个数字,它可能有一个负号在最前面,你就可以写一个负号加上一个问号,像这样:-?
在正则表达式中,用\d表示一位数字。Java对反斜杠的处理与其他语言不同。在其他语言中,\表示“我想要在正则表达式中插入一个普通的反斜杠,不要给它任何特殊意义。”而在Java中,\的意思是“我要插入一个正则表达式的反斜杠,所以其后面的字符具有特殊意义。”
例如,你想要表示一位数字,那么正则表达式应该是\d。如果你想插入一个普通的反斜杠,则应该这样\\。原因是:在其他的语言中,一个反斜杠\就足以具有转义的作用,而在java正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单理解为,java正则表达式中的两个\代表其他语言中的一个\,这也就是为什么表示一位数字的正则表达式是\d,而表示一个普通的反斜杠是\\。
要表示:“一个或者多个之前的表达式”,应该使用+。所以要表示“可能有一个负号,后面跟着一位或多位数字”,可以这样:-?\d+
例子:
public class Test {
public static void main(String[] args) {
System.out.println("-1234".matches("-?\\d+"));
System.out.println("5678".matches("-?\\d+"));
System.out.println("+123".matches("-?\\d+"));
System.out.println("+456".matches("(-|\\+)?\\d+"));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
输出:
true
true
false
true
上例中,前两个字符串满足对应的正则表达式,匹配成功。第三个字符串开头有一个+,它也是一个合法的整数,但与对应的正则表达式却不匹配。因此,我们地正则表达式应描述为:“可能以一个加号或减号开头”。在正则表达式中,括号有着将表达式分组的效果,而竖线|则表示或操作。(-|\+)?表示字符串起始字符可能是一个-或+,或二者皆没有(因为后面跟着?修饰符)。因为字符+在正则表达式中有特殊意义,所以必须使用\将其转义,使之成为表达式中的一个普通字符。
String类还自带了一个非常有用的正则表达式工具——split()方法,其功能是“将字符串从正则表达式匹配的地方切开。”
例子:
import java.util.Arrays;
public class Test8 {
public static String s="I think! An individual human...existence should be like a river.";
public static void splitTest(String regex) {
System.out.println(Arrays.toString(s.split(regex)));
}
public static void main(String[] args) {
splitTest(" ");
splitTest("\\W+");//非单词字符
splitTest("n\\W+");//字母n后面跟着一个或多个非单词字符
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
输出:
[I, think!, An, individual, human…existence, should, be, like, a, river.]
[I, think, An, individual, human, existence, should, be, like, a, river]
[I think! A, individual huma, existence should be like a river.]
上例中,第一个split()按空格来划分字符串。第二个split()按非单词字符划分,可看出它把标点字符都删除了。第三个split()表示"字母n后面跟着一个或多个非单词字符",可看出,在原始字符中,匹配的部分都在结果中不存在了。
String类自带的最后一个正则表达式工具是“替换”。你可以只替换正则表达式第一个匹配的子串,或是替换所有匹配的地方。
例子:
public class Test { public static String s="Whatever is worth doing is worth doing well.";
public static void main(String[] args) { System.out.println(s.replaceFirst("i\\w+", "was")); System.out.println(s.replaceAll("Whatever|worth", "banana")); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
输出:
Whatever was worth doing is worth doing well.
banana is banana doing is banana doing well.
上例中,第一个表达式匹配的是,以i开头,后面跟着一个或多个字母(注意这里w是小写)。并且只替换掉第一个匹配的部分,所以,“is”被替换成了“was”。第二个表达式要匹配的是两个单词的任意一个,因为它们以竖线分隔表示“或”,并且替换所有匹配部分。
(2)创建正则表达式
字符:
B 指定字符B
\t 制表符Tab
\n 换行符
\r 回车
\f 换页
\e 转义
- 1
- 2
- 3
- 4
- 5
- 6
字符类:
. 任意字符
[abc] 包含a、b、c的任何字符(和a|b|c左右相同)
[^abc] 除了a、b、c外的任何字符
[a-zA-Z] 从a到z或从A到Z的任何字符
[abc[hij]] 任意a、b、c、h、i、j字符(和a|b|c|h|i|j左右相同)
[a-z&&[hij]] 任意h、i、j字符
\s 空白符
\S 非空白符[^\s]
\d 数字[0-9]
\D 非数字[^0-9]
\w 词字符[a-zA-Z0-9]
\W 非词字符[^\w]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
逻辑操作符:
XY Y跟在X后面
X|Y X或Y
- 1
- 2
边界匹配符:
^ 一行的开始
$ 一行的结束
\b 词的边界
\B 非词的边界
\G 前一个匹配的结束
- 1
- 2
- 3
- 4
- 5
例子:
public class Test { static String[] arr= {"Rudolph","[rR]udolph","[rR][aeiou][a-z]ol.*","R.*"};
public static void main(String[] args) { for(String s:arr) { System.out.println("Rudolph".matches(s)); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
输出:
true
true
true
true
5、扫描输入
例子:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
public class Test {
public static BufferedReader input=new BufferedReader(new StringReader(
“Sir Robin of Camelot\n22 1.61803”));
public static void main(String[] args) {
try {
System.out.println(“What is you name?”);
String name=input.readLine();
System.out.println(name);
System.out.println(“How old are you?What is you favorite double?”);
System.out.println(“input:<age> <double>”);
String numbers=input.readLine();
System.out.println(numbers);
String []numArr=numbers.split(" ");
int age=Integer.parseInt(numArr[0]);
double favorite=Double.parseDouble(numArr[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.”);
}
}
}
- 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
输出:
What is you name?
Sir Robin of Camelot
How old are you?What is you favorite double?
input:
22 1.61803
Hi Sir Robin of Camelot.
In 5 years,you will be 27.
My favorite double is 0.809015.
上例中,StringReader将String转换为可读的流对象,然后用这个对象来构造BufferedReader对象,因为我们要使用BufferedReader的readLine()方法。最终,我们可以使用input对象一次读取一行文本,就像是从控制台读入标准输入一样。readLine()方法将一行输入转为String对象。如果每一行数据正好对应一个输入值,那这个方法也可还行。但,若两个输入值在同一行中,事情就不好办了,我们必须分解这个行,才能分别翻译所需的输入值。在这个例子中,分解的操作发生在创建numArr时。
java SE5新增了Scanner类,它可以大大减轻扫描输入的工作负担:
例子:
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner in=new Scanner("Sir Robin of Camelot\n22 1.61803");
System.out.println("What is your name?");
String name=in.nextLine();
System.out.println(name);
System.out.println("How old are you?What is you favorite double?");
System.out.println("input:<age> <double>");
int age=in.nextInt();
double favorite=in.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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
输出:
What is your name?
Sir Robin of Camelot
How old are you?What is you favorite double?
input:
22
1.61803
Hi Sir Robin of Camelot.
In 5 years,you will be 27.
My favorite double is 0.809015.
(1)Scanner定界符
默认情况下,Scanner根据空白字符对输入进行分词,但你也可以用正则表达式指定自己所需的定界符:
#####例子:
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner in=new Scanner("12,43,45,67,38");
in.useDelimiter("\\s*,\\s*");
while(in.hasNextInt()) {
System.out.println(in.nextInt());
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
输出:
12
43
45
67
38
6、StringTokenizer
在java引入正则表达式和Scanner类之前,分割字符串的唯一方法是使用StringTokenizer来分词。不过,现在有了正则表达式和Scanner,我们可以使用更加简单、简洁的方式来完成同样的工作了。
例子:
import java.util.Arrays; import java.util.Scanner; import java.util.StringTokenizer; public class Test {
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()+" "); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
输出:
But I’m not dead yet! I feel happy!
[But, I’m, not, dead, yet!, I, feel, happy!]
But I’m not dead yet! I feel happy!
十三、泛型
java SE5重大变化之一:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。
1、简单泛型
有许多原因促成了泛型的出现,而最引人注目的原因,就是为了创造容器类。容器,就是要存放对象的地方。
有些情况下,我们确实希望容器能够持有多种类型的对象。但是通常而言,我们只会使用容器来存储一种类型的对象。泛型的主要目的之一就是来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。下面例子中,T就是类型参数:
public class Holder<T> {
private T a;
public Holder(T a) {this.a=a;}
public void set(T a) {this.a=a;}
public T get() {return a;}
public static void main(String[] args) {
Holder<Automobile> holder=new Holder<Automobile>(new Automobile());
Automobile a=holder.get();//No cast needed
// holder.set("Not an Automobile");//error
// holder.set(1);//error
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
上例中,当你创建Holder对象时,必须指定想持有什么类型的对象,将其置于尖括号内。就像main()中那样。然后,你就只能在Holder中存入该类型的对象了。而且,在你从Holder中取出它持有的对象时,自动就是正确的类型。
2、泛型类型
要定义泛型方法,只需将泛型参数列表置于返回值之前:
public class Test<T> {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
Test t=new Test();
t.f("abc");
t.f(1);
t.f(1.0);
t.f(1.0f);
t.f('c');
t.f(t);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
输出:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
Test
十四、数组
1、数组的特殊性
在java中,数组是一种效率最高的存储和随机访问对象引用序列的方式。但是这种速度所付出的代价是数组对象大小固定,且在其生命周期中不可改变。List和数组唯一明显的差异就是数组使用[]来访问元素,而List使用的是add()和get()方法。
随着自动包装机制的出现,容器已经可以与数组几乎一样方便地用于基本类型中了。数组仅存的优点就是效率。然而,如果要解决一般化的问题,那数组就受到过多的限制,因此在这种情况下你还是会使用容器。
2、多维数组
对于基本类型的多维数组,可以通过使用花括号将每个向量分隔开。
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
int [][] a= {{1,2,3},{4,5,6},{7,8,9}};
System.out.println(Arrays.deepToString(a));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
输出:
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
int [][][] a= new int[2][2][4];
System.out.println(Arrays.deepToString(a));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
输出:
[[[0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]]
上例中,使用了java SE5的Arrays.deepToString()方法,它可以将多维数组转换为多个String。还可以用new来分配数组,基本类型数组的值在不进行显示初始化的情况下,会被自动初始化为0。对象数组会被初始化为null。
3、创建测试数据
Arrays.fill()
java标准类库Arrays有一个作用十分有限的fill()方法,只能用一个值填充各个位置,而针对对象数组而言,就是复制同一个引用进行填充。
import java.util.Arrays;
public class B{
public static void main(String[] args) {
int [] a= new int[7];
boolean [] b=new boolean[7];
Arrays.fill(a, 6);
Arrays.fill(b, true);
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
输出:
[6, 6, 6, 6, 6, 6, 6]
[true, true, true, true, true, true, true]
但是由于只能用单一的数值来调用Arrays.fill(),因此所产生的结果并非特别有用。
数据生成器
为了以灵活的方式创建更有意义的数组,引入Generator的概念。若使用了Generator,你就可以通过选择Generator的类型来创建任何类型的数据。
首先给出的是可以用于所有基本类型的的包装器类型,以及String类型最基本的计数生成器集合。
这些生成器都嵌套在CountingGenerator类中,从而使得它们能够使用和所要生成的对象类型相同的名字。例如,创建Integer对象的生成器可以通过表达式new CountingGenerator.Integer()来创建
4、Arrays实用功能
在java.util类库中可以找到Arrays类,它有一套用于数组的static方法,其中有6个基本方法:
——equals():用于比较两个数组是否相等(deepEquals()用于多维数组)
——fill():用一个值填充数组的各个位置
——sort():用于对数组进行排序
——binarySearch():用于在已经排好序的数组中查找元素
——toString():产生数组的String表示
——hashCode():产生数组的散列码(哈希码)
此外,Arrays.asList()接受任意的序列或者数组作为其参数,并将其转换为List容器
- 1
- 2
- 3
- 4
- 5
- 6
- 7
复制数组
java标准类库提供有static方法System.arraycopy(),用它复制数组比for循环复制要快得多。System.arraycopy()针对所有类型做了重载。
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
//基本类型数组
int [] i=new int[7];
int [] j=new int[10];
Arrays.fill(i, 47);
Arrays.fill(j, 99);
System.out.println("i="+Arrays.toString(i));
System.out.println("j="+Arrays.toString(j));
System.arraycopy(i, 0, j, 0, i.length);
System.out.println("j="+Arrays.toString(j));
int [] k=new int[5];
Arrays.fill(k, 13);
System.arraycopy(i, 0, k, 0, k.length);
System.out.println("k="+Arrays.toString(k));
Arrays.fill(k, 13);
System.arraycopy(k, 0, i, 0, k.length);
System.out.println("i="+Arrays.toString(i));
//对象数组
Integer [] u=new Integer[10];
Integer [] v=new Integer[5];
Arrays.fill(u, new Integer(47));
Arrays.fill(v, new Integer(99));
System.out.println("u="+Arrays.toString(u));
System.out.println("v="+Arrays.toString(v));
System.arraycopy(v, 0, u, u.length/2, v.length);
System.out.println("u="+Arrays.toString(u));
}
}
- 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
输出:
i=[47, 47, 47, 47, 47, 47, 47]
j=[99, 99, 99, 99, 99, 99, 99, 99, 99, 99]
j=[47, 47, 47, 47, 47, 47, 47, 99, 99, 99]
k=[47, 47, 47, 47, 47]
i=[13, 13, 13, 13, 13, 47, 47]
u=[47, 47, 47, 47, 47, 47, 47, 47, 47, 47]
v=[99, 99, 99, 99, 99]
u=[47, 47, 47, 47, 47, 99, 99, 99, 99, 99]
arraycopy()需要的参数有:源数组、表示从源数组中的什么位置开始复制的偏移量、目标数组、表示从目标数组的什么位置开始复制的偏移量,以及需要复制的元素个数。
上例说明基本类型数组和对象数组都可以复制。然而,如果要复制对象数组,那么只是复制了对象的引用——而不是对象本身的拷贝。这称为浅拷贝。
数组的比较
Arrays类提供了重载的equals()方法,用来比较整个数组。数组相等的条件是元素个数必须相等,而且对应位置的元素也相等,这可以通过对每个元素使用equals()作比较来判断。(对于基本类型,需要使用基本类型的包装器类的equals()方法,例如,对于int类型使用Integer.equals()作比较)。
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
int[] a1=new int[10];
int[] a2=new int[10];
Arrays.fill(a1, 47);
Arrays.fill(a2, 47);
System.out.println(Arrays.equals(a1, a2));
a2[3]=11;
System.out.println(Arrays.equals(a1, a2));
String[] s1=new String[4];
Arrays.fill(s1, "Hi");
String[] s2= {new String("Hi"),new String("Hi"),new String("Hi"),new String("Hi")};
System.out.println(Arrays.equals(s1, s2));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
输出:
true
false
true
上例中,最初a1和a2是完全相等的,所以输出为true;然后改变了a2
的一个元素,使得结果为false。在最后的例子中,s1所有元素都指向同一个对象,然而s2包含5个相互独立的对象。由于数组相等是基于内容的,所以输出为true。
数组元素的比较
Java有两种方式来提供比较功能。第一种是实现java.lang.Comparable接口,使你的类具有“天生”的比较能力。此接口很简单,只有compareTo()一个方法。此方法接收另一个Object为参数,如果当前对象小于参数则返回负值,如果相等则返回零,如果当前对象大于参数则返回正值。
public class Test{
public static void main(String[] args) {
Integer [] i= {2,2,3};
Integer [] j= {1,2,4};
for(int k=0;k<i.length;k++) {
System.out.println(i[k].compareTo(j[k]));
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
输出:
1
0
-1
Collection类包含了一个reverseOrder()方法,该方法可以产生一个Comparator,它可以反转自然的排序顺序。
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Test{
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add(13);
list.add(1);
list.add(7);
list.add(9);
Comparator cmp=Collections.reverseOrder();
Collections.sort(list, cmp);
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
输出:
13
9
7
1
数组排序
使用内置的排序方法,就可以对任意的基本类型数据排序;也可以对任意的对象数组进行排序,只要该对象实现了Comparable接口或具有相关联的Comparator。
import java.util.Arrays;
import java.util.Collections;
public class Test{
public static void main(String[] args) {
String[] s= {“asd”,“ksd”,“ejg”,“gjh”};
System.out.println(“Before sort:”+Arrays.toString(s));
Arrays.sort(s);
System.out.println(“After sort:”+Arrays.toString(s));
Arrays.sort(s,Collections.reverseOrder());
System.out.println(“Reverse sort:”+Arrays.toString(s));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
输出:
Before sort:[asd, ksd, ejg, gjh]
After sort:[asd, ejg, gjh, ksd]
Reverse sort:[ksd, gjh, ejg, asd]
在已排序数组中查找
如果数组已经排好序,就可以使用Arrays.binarySearch()执行快速查找。如果要对未排序的数组使用binarySearch(),那么将产生不可预料的结果。
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
int a[] = new int[] {1, 4, 5, 6, 8, 9};
int x1 = Arrays.binarySearch(a, 5);
int x2 = Arrays.binarySearch(a, 4);
int x3 = Arrays.binarySearch(a, 0);
int x4 = Arrays.binarySearch(a, 7);
System.out.println(“x1:”+x1+",x2:"+x2+",x3:"+x3+",x4:"+x4);
int x5 = Arrays.binarySearch(a, 1, 4, 5);
int x6 = Arrays.binarySearch(a, 1, 3, 7);
int x7 = Arrays.binarySearch(a, 1, 4, 0);
int x8 = Arrays.binarySearch(a, 1, 3, 10);
System.out.println(“x5:”+x5 +",x6:"+x6+",x7:"+x7+",x8:"+x8);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
输出:
x1:2,x2:1,x3:-1,x4:-5
x5:2,x6:-4,x7:-2,x8:-4
技巧:
binarySearch(Object[], Object key)
[1] 搜索值不是数组元素,且在数组范围内,从1开始计数,得“ - 插入点索引值”;
[2] 搜索值是数组元素,从0开始计数,得搜索值的索引值;
[3] 搜索值不是数组元素,且大于数组内元素,索引值为 – (length + 1);
[4] 搜索值不是数组元素,且小于数组内元素,索引值为 – 1。
binarySearch(Object[], int fromIndex, int toIndex, Object key)
[1] 该搜索键在范围内,但不是数组元素,由1开始计数,得“ - 插入点索引值”;
[2] 该搜索键在范围内,且是数组元素,由0开始计数,得搜索值的索引值;
[3] 该搜索键不在范围内,且小于范围(数组)内元素,返回–(fromIndex + 1);
[4] 该搜索键不在范围内,且大于范围(数组)内元素,返回 –(toIndex + 1)。
十五、容器的深入研究
1、填充容器
虽然容器打印的问题解决了,容器的填充仍像java.util.Arrays一样面临同样的不足。像Arrays一样,相应的Collections类也有一些实用的static方法,其中包括fill()。Arrays.fill与Collections.fill不一样,Arrays.fill是用同一个值填充数组的各个位置,而Collections.fill是使用指定元素替换指定列表中的所有元素。
import java.util.*;
class stringAddress{
private String s;
public stringAddress(String s){
this.s=s;
}
public String toString() {
return super.toString()+" "+s;
}
}
public class FillingLists{
public static void main(String[] args) {
List<stringAddress> list=new ArrayList<stringAddress>(Collections.nCopies(3, new stringAddress("Hello")));
System.out.println(list);
Collections.fill(list, new stringAddress("World!"));
System.out.println(list);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
输出:
[stringAddress@15db9742 Hello, stringAddress@15db9742 Hello, stringAddress@15db9742 Hello]
[stringAddress@6d06d69c World!, stringAddress@6d06d69c World!, stringAddress@6d06d69c World!]
上例中展示了用单个对象的引用来填充Collection的两种方式,第一种是使用Collections.nCopies(),其含义是返回大小为n(这里是3)的List,List不可改变,其中的所有引用都指向对象Hello;第二种是用Collections.fill(),其含义是用对象World替换集合list中的所有元素。
stringAddress的toString()方法调用Object.toString()并产生该类的名字,后面紧跟着该对象的散列码无符号十六进制表示。
Collection生成器
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
interface Generator<T> {
T next();
}
class CollectionData<T> extends ArrayList<T> {
public CollectionData(Generator<T> gen, int quantity) {
for (int i = 0; i < quantity; i++)
add(gen.next());
}
public static <T> CollectionData<T> list(Generator<T> gen, int quantity) {
return new CollectionData<T>(gen, quantity);
}
}
class Animal implements Generator<String> {
private String[] items = { “Dog”, “Pig”, “Cat” };
private int index;
@Override
public String next() {
return items[index++];
}
}
public class Test {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>(new CollectionData<String>(new Animal(), 3));
System.out.println(set); // [Dog, Pig, Cat]
set.addAll(CollectionData.list(new Animal(), 3));
System.out.println(set); // [Dog, Pig, Cat]
}
}
- 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
输出:
[Dog, Pig, Cat]
[Dog, Pig, Cat]
上例中,这个类使用Generator在容器中放置所需数量的对象,然后所产生的容器可以传递给任何Collection的构造器,这个构造器会把其中的数据复制到自身中。addAll()方法是所有Collection子类型的一部分,它也可以用来组装现有的Collection。
2、Collection的功能方法
—boolean add(T):确保容器持有具有泛型类型T的参数
—boolean addAll(Collection<? extends T>):添加参数中的所有元素
—void clear():移除容器中所有元素
—boolean contains(T):如果容器已经持有具有泛型类型T此参数,则返回true
—boolean containsAll(Collection<?>):如果容器持有此参数中的所有元素,则返回true
—boolean isEmpty():容器中没有元素时返回true
—Iterator<T>iterator():返回一个Iterator<T>,可以用来遍历容器中的元素
—boolean remove(Object):如果参数在容器中,则移除此参数的一个实例。如果做了移除动作,则返回true
—boolean removeAll(Collection<?>):移除参数中的所有元素。只要有移除动作发生就返回true
—boolean retainAll(Collection<?>):只保留参数中的元素。只要Collection发生改变就返回true
—int size():返回容器元素个数
—Object[] toArray():返回一个数组,该数组包含容器中的所有元素
—<T> T[] toArray(T[] a):返回一个数组,该数组包含容器中的所有元素,返回结果的运行时类型与参数数组a的类型相同,而不是单纯的Object
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
下面的例子展示了上面的方法:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
public class Test8 {
public static void main(String[] args) {
Collection<String> c=new ArrayList<String>();
Collection<String> c2=new ArrayList<String>();
Collection<String> c3=new ArrayList<String>();
c.add(“h”);
c.add(“f”);
c2.add(“c”);
c2.add(“s”);
System.out.println(“c=”+c);
System.out.println(“c2=”+c2);
c.addAll(c2);
System.out.println(“c add c2=”+c);
System.out.println(“Collections.max©=”+Collections.max©);
System.out.println(“Collections.min©=”+Collections.min©);
c.remove(“f”);
System.out.println(“f remove from c=”+c);
c.removeAll(c2);
System.out.println(“c2 remove from c=”+c);
System.out.println(“c contains h=”+c.contains(“h”));
c3.add(“a”);
c3.add(“c”);
c3.add(“s”);
System.out.println(c3);
System.out.println(“c3 contains c2=”+c3.containsAll(c2));
c3.clear();
System.out.println(“c3 after clear=”+c3);
}
}
- 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
输出:
c=[h, f]
c2=[c, s]
c add c2=[h, f, c, s]
Collections.max©=s
Collections.min©=c
f remove from c=[h, c, s]
c2 remove from c=[h]
c contains h=true
[a, c, s]
c3 contains c2=true
c3 after clear=[]
3、List的功能方法
基本的List很容易使用:大多数时候只是调用add()添加对象,使用get()一次取出一个元素,以及调用iterator()获取用于该序列的Iterator。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public static void main(String[] args) {
List list = new ArrayList();
list.add(“a”);
list.add(1, “b”);
list.addAll(list);
list.addAll(4, list);
int i = list.size();
System.out.println(i);
list.get(0);
list.remove(7);
System.out.println(list.contains(“a,b,c”));
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
System.out.println(list.isEmpty());//false
System.out.println(null!=list);//true
System.out.println(list.indexOf("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
- 27
输出:
8
false
a
b
a
b
a
b
a
false
true
0
4、set和存储顺序
当你创建自己的类型时,要意识到Set需要一种方式来维护存储顺序,而存储顺序如何维护,则是在Set的不同实现之间有所变化。因此,不同的Set实现不仅具有不同的行为,而且他们对于可以在特定的Set中放置的元素的类型也有不同的要求:
—Set(interface):存入Set的每个元素必须是唯一的,不保留重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序
—HashSet*:为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()
—TreeSet:保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable接口
—LinkedHashSet:具有HashSet的查询速度,且内部使用链表维护元素顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。元素也必须定义hashCode()方法
在HashSet上打星号表示,如果没有其他限制,这就应该是你默认的选择,因为它对速度进行了优化。
- 1
- 2
- 3
- 4
- 5
你必须为散列存储和树型存储都创建一个equals()方法,但是hashCode()只有在这个类将会被置于hashSet或LinkedHashSet时才是必需的。但是,对于良好的编程风格而言,你应该在覆盖equals()方法时,总是同时覆盖hashCode()方法。
(1)SortedSet
SortedSet中的元素可以保证处于排序状态,这使得它可以通过在SortedSet接口中的下列方法提供附加功能:Comparator comparator()返回当前Set使用的Comparator;或者返回null,表示以自然方式排序。
Object first()返回容器中的第一个元素
Object last()返回容器中的最后一个元素
SortedSet subSet(fromElement,toElement)生成此Set子集,范围从fromElement(包含)到toElement(不包含)。
SortedSet headSet(toElement)生成此Set的子集,由小于toElement的元素组成。
SortedSet tailSet(fromElement)生成此Set的子集,由大于或等于fromElement的元素组成。
import java.util.Collections;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
public class Test {
public static void main(String[] args) {
SortedSet<String> sortedSet=new TreeSet<String>();
Collections.addAll(sortedSet, “one two three four five six seven”.split(" "));
System.out.println(sortedSet);
String low=sortedSet.first();
String high=sortedSet.last();
System.out.println(low);
System.out.println(high);
Iterator<String> it=sortedSet.iterator();
for(int i=0;i<=5;i++) {
if(i1) low=it.next();
if(i4) high=it.next();
else it.next();
}
System.out.println(low);
System.out.println(high);
System.out.println(sortedSet.subSet(low, high));
System.out.println(sortedSet.headSet(high));
System.out.println(sortedSet.tailSet(low));
}
}
- 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
输出:
[five, four, one, seven, six, three, two]
five
two
four
three
[four, one, seven, six]
[five, four, one, seven, six]
[four, one, seven, six, three, two]
</div>
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-8cccb36679.css" rel="stylesheet">
</div>