断断续续终于把男神的Java都看完了,只是囫囵吞枣走马观花罢了,还有好多地方不是很理解,很多细节都没能理清。之后再边动手边复习一遍,然后开始尝试做点东西吧!
0. 一些小tips
·为避免出现错误提示:The local variable may not have been initialized.应该每个函数内的局部变量都定义初始化。即保持良好的编程习惯,每个变量都定义初始化。
·String s = “”;
·boolean b = false;
·int a = 0;
·为避免构造方法的歧义重载,良好的编程习惯是默认建立一个空的构造方法:
<类名>()
{
}
·每一个类中都可以写一个main方法,用来测试这个类的一些功能和属性
·时刻谨记对象变量只是对象的管理者而非对象本身,声明对象变量并没有创建对象,就好比声明一个指针,指针并不会指向任何有意义的内存空间一样。对象变量和指针变量,两者都需被具体赋值,才能进行接下来的操作。
·需要对字符串进行很多复杂操作并会产生大量新的字符串时,为减少系统开销,应使用StringBuffer,最后用toString()输出结果字符串即可
1. 类与对象
·对象是实体,需要被创建,可以为我们做事情
·类是规范,根据类的定义来创建对象
类定义了对象,而对象是类的实体。
对象 = 属性 + 服务
封装:把数据和对数据的操作放在一起,由操作来保护数据,数据不对外公开。
其中,
·数据:属性或状态 // 类的成员变量
·操作:函数 // 类的成员函数
定义一个类,即是封装的过程:
类中的变量都是类的成员变量(数据),类中的函数都是类的成员函数(对数据的操作)。
可以形象地将类描述成一个切开一半的咸蛋:蛋黄是数据,而包裹在蛋黄外面的蛋白则是对数据的操作,由蛋白包裹覆盖蛋黄以起到保护的作用,就是用对数据的操作来保护数据。
用类创建(new)一个对象,语法上表达为:
<类> <对象名> = new <类>();
定义出来的变量是对象变量,是对象的管理者而非所有者(即对象变量不是对象本身,类似指针与被指针指向的内存区域的关系)。
一个类可以任意创建多个对象,每个对象都是独立不同的。在Debug中可以看到每个对象变量的细节:对象内部的变量当前的值,对应的value项会显示对象所属的类名以及id等。
在创建对象变量之后,就可以用.运算符进行对象的操作了,即调用对象的某个函数,写法为:
<对象>.<对象的成员函数名>();
1)函数与成员变量
在函数中可以直接写成员变量的名字来访问成员变量,就好比C中的函数可以直接用全局变量的名字一样。
函数是通过对象来调用的:
·v.insertMoney();
·这次调用临时建立了insertMoney()和V之间的关系,让insertMoney()内部的成员变量指的是v的成员变量。
this是成员函数的一个特殊的固有本地变量,它表达了调用这个函数的那个对象(可以用指针的思想来理解:有一个固有的对象管理者this,函数调用时this临时指向了.前面的那个对象,以表达当前调用函数的具体是哪个对象)
this可以写在函数内部,用来表示此次调用该函数的那个对象,因此,
·可以用this来区分同名的 对象的成员变量 与 函数内部的本地变量:
class A { int a; void function( int a ) { this.a = a; // 若不用this则语法上是错误的,this.a表达的是对象的成员变量,而a则是函数function()内的参数,即一个本地变量,两者是不同的 } }
·也可以用来方便地(偷懒少打几个字)来调用其他成员函数:
1 class B { 2 int a; 3 … ; 4 5 void function1() 6 { 7 this.function2(); //等同于function2();在eclipse上输入.之后会列出该类中的全部成员函数,然后选择所需即可自动填补 8 9 } 10 void function2() 11 { 12 … ; 13 } 14 }
2)本地变量与成员变量
·定义在函数内部的是本地变量,其生存期和作用域都是函数内部
·成员变量是定义在类内函数外的,生存期是对象的生存期,作用域是类内部
3)成员变量初始化
在Java中,不允许存在未使用的本地变量,而成员变量的初始化则是自动的。
·成员变量在定义的地方就可以给初始值
·没有初始值的成员变量会自动获得0值 // 这里的0值是广义的:0/false/null
·对象变量的0值表示它没有管理任何对象,也可以主动给null值
·定义初始化时可以使用调用函数的返回值,甚至可以使用已经定义过的成员变量的值
4)构造函数与函数重载
如果有一个成员函数的名字与类名完全相同,则在创建这个类的每一个对象的时候会自动调用这个函数,即构造函数,也称构造器。
构造函数:在构造(创建)对象的时候会被自动调用的函数,执行时先执行构造函数外面的定义初始化,之后再进入构造函数,最后构造函数结束返回new处,将构造出来的对象交给对象变量管理。构造函数不能有返回类型(包括void,void可以理解为这个函数有返回类型,类型是空(void),没有返回值)。
在Java中,函数可以重载:
·一个类可以有多个构造函数,只要它们的参数表不同
·创建对象的时候给出不同的参数值,就会自动调用不同的构造函数
·通过this()还可以调用其他构造函数
·一个类里的同名但参数不同的函数构成了重载关系
关于this():只能在构造函数内的第一句处使用一次。写法:
1 class A { 2 int a; 3 … ; 4 A() // 构造函数,与类名完全一致且无返回类型 5 { 6 … ; 7 } 8 A( int a ) // 多个同名但参数表不同的函数实现函数重载 9 { 10 this(); //在A(int a)内重载A(),只能在函数内第一句处使用一次 11 … ; 12 } 13 }
2. 对象交互
面向对象程序设计的第一步,就是在问题领域中识别出有效的对象,然后从识别出的对象中抽象出类来。
面向对象的核心在于看到在问题中有些什么样的东西,每样东西有什么样的属性,这些东西之间是如何交互的。
一个类中的成员变量可以使其他类的对象,最后得到的该类的对象是由多个其他类的对象组成的(就好比C中的结构体嵌套结构体一样,Java中是对象包含对象,但Java中的对象能做的事情比C中的结构体多太多了)。
面向对象设计思想:
对象和对象之间的联系紧密程度叫做耦合。对象和对象的耦合程度越紧,表现在源代码上,就是它们的代码是互相依赖、互相牵制的。我们理想的模型,是对象和对象之间的耦合要尽可能的松,平行的对象要尽量减少直接联系,让更高层次的对象来提供通信服务。
即,同一个类的每一个对象都尽可能的相互独立,彼此之间没有直接联系。这样在编写对象甲的时候就不用去考虑对象乙的情况,编写对象乙的时候也不需要考虑对象甲。让两者之间的交互由更高级的对象丙(同时包含对象甲和对象乙)来完成。
封装,就是把数据和对这些数据的操作放在一起,并且用这些操作把数据掩盖起来,是面向对象的基本概念之一,也是最核心的概念。
一个非常直截了当的手段来保证在类的设计的时候做到封装:
·所有的成员变量必须是private的,这样就避免别人任意使用你的内部数据;
·所有public的函数,只是用来实现这个类的对象或类自己要提供的服务的,而不是用来直接访问数据的。除非对数据的访问就是这个类及对象的服务。简单地说,给每个成员变量提供一对用于读写的get/set函数也是不合适的设计。
封闭的访问属性:private(私有的):
·只有这个类的内部可以访问(即如果在别的类中创造了该类的对象,也无法通过这个对象来访问private的成员变量,因为不是在该类的内部)
·类的内部指的是成员函数和定义初始化
·这个限制是针对类的而不是针对对象的:即,对于私有的理解是从代码的层面而非运行的层面上来看的。只要对private的成员变量的访问是在该类的内部发生的,这个操作就是合理的。也就是说,同一个类的不同对象之间可以互相访问私有成员。
开放的访问属性:public(公共的):
·任何部分(其他类)都可以*访问
如果一个类是public的:
·表明任何类都可以利用该类定义变量
·该类所处源文件的文件名必须与该类名相同,否则会报错:The public type <class name> must be defined in its own file.修改建议为:Rename compilation unit to ‘<class name>.java’
编译单元:
·一个源代码文件(.java文件)是一个编译单元(compilation unit),编译时一次仅对一个编译单元进行编译。
·一个编译单元中可以有很多Java类,但只能有一个类是public的。如果类不是public的,则该类只能在所处的包中起作用。
若在成员前未加任何关键字来修饰限定它,那么表明该成员是friendly的,意为与该成员所属类位于同一个包(package)的其他类可以访问它。
包:
包是Java的类库管理机制,它借助文件系统的目录来管理类库,一个包就是一个目录,一个包内的所有的类必须放在一个目录下,那个目录的名字必须是包的名字。
每一个Java源文件的第一个语句一定是package <包名>;这句话表明了这个public类属于哪个包,也就是与public类同名的Java源文件位于那个与包同名的文件夹下。在Eclipse中,如果一个文件中没有package语句,则表明该类属于一个没有名字的默认包(default package)。
如果在一个类中使用了另外的与其不在同一个包中的类,则有两种方式:
1)在使用该类的时候写出类的全名:<包名>.<类名>
2)使用import,写法如下:
import <包名>.<类名>(即类的全名)
这样写的好处是:
清晰:在查找其他包的类时只需要看import处的那一列即可。
方便:在一个类中使用import的类只需要写类名而不用写类的全名。
同时import还可以写成:
import <包名>.*(此处的*的含义是通配符)
这样的写法表示import该包里的全部类。一般来说不建议这种写法,因为有可能会发生重名冲突。
注意,即使import过所需其他包的类,也无法访问该类的friendly成员,因为friendly成员只能被同一个包的其他类访问,不同包则不能。
包中包:
在建立新的包时如果起名为<已有包>.<包名>,则意味着这个新的包是一个包中包,在文件系统里体现为与该包同名的文件夹是已有包同名文件夹的子文件夹,在import类时则写成:
import <包名>.<包名>.<类名>;
此时两个包之间的.就等同于文件目录路径中的\
具体体现在文件系统中的层级依次为:
workspace
project
src
package
*.java
bin
package
*.class
例:
workspace
Project_1 // 文件夹名与项目名一致,为Project_1
src // 文件夹名即src,按包分类存放.java文件(源文件)
clock // 文件夹名与包名一致,为clock
Clock.java // 文件名与类名一致,为Clock
Display.java // 文件名与类名一致,为Display
time // 文件夹名为time,该包即是包中包,包名为clock.time
Time.java // 文件名与类名一致,为Time,该类全名为clock.time.Time
bin // 文件夹名即bin,按包分类存放.class文件(字节码文件)
clock
Clock.class
Display.class
time
Time.class
Project_2
src
a.java // 如果是默认包,则文件直接保存在src目录下
bin
- class
在以上的情况下,如果在Clock.java文件中想要使用Time类,则应该在Clock类的前面写import clock.time.Time; // 包的路径是 ..\clock\time
static,类变量与类函数:
类是描述,对象是实体。在类里所描述的成员变量,是位于这个类的每一个对象中的。
而如果某个成员有static关键字做修饰,它就不再属于每一个对象,而是属于整个类的了。
通过每个对象都可以访问到这些类变量和类函数,但是也可以通过类的名字来访问它们。类函数由于不属于任何对象,因此也没有办法建立与调用它们的对象的关系,就不能访问任何非static的成员变量和成员函数了。
static与public和private一样,是用来修饰变量或函数的关键字。
static意味着它所修饰的那个东西不属于类中的任何对象,而属于这个类。
在类体中,如果定义一个变量/函数,在前面加上关键字static,则表明这个变量/函数是类变量/函数而非成员变量/函数:
·成员变量/函数位于每个对象之中而非这个对象的类之中,即同一个类的每个对象拥有属于自己的成员变量/函数,对象与对象之间的成员变量/函数相互独立没有联系;
·而static变量/函数则是类变量/函数,类变量/函数属于整个类,而不属于这个类的任何一个对象,但每个对象都可以访问到它。
有两种方式可以访问类变量/函数:
<对象名>.<类变量/函数名>或<类名>.<类变量/函数名>
前者是通过对象来访问类变量/函数,因此只要属于同一个类,任何对象都可以这样写。
后者是通过类来访问类变量/函数,更建议使用这种写法,以便与对象的成员变量/函数清晰地区分开来。
关于static的规则:
·如果一个函数是static的,那么它只能访问static变量/函数
·static的变量和函数既可以通过类名来访问,也可以通过某个对象名访问,但通过对象名访问时并不能获得该对象的具体信息。
·如果在定义static变量时定义初始化,那么这个初始化只会在加载这个类的时候(即创建类变量,为其分配内存空间时)做一次,而与对象的创建无关。
3. 容器
程序设计的思想之一:人机交互与业务逻辑分离开来。在做业务逻辑的时候,只专注于数据本身,而不要去考虑人机交互方面的东西。因此,对于一个数据的读取,不要在对象的方法里用单纯的System.out.println()将它输出,而是要把数据返回,由上层调用这个对象的函数来决定,得到这个返回值之后该做什么操作。这样一方面使得函数的用法更加灵活(而不单单只能用来输出),另一方面更符合面向对象程序设计的思想。人机交互与业务逻辑的统一标准由接口的设计来实现,做项目的时候,先定义接口,再实现具体功能。
1)顺序容器:ArrayList<Type>
·ArrayList<String> notes = new ArrayList<String>;
意为创建一个array list of string类型的对象变量notes
·ArrayList是系统类库中的一个泛型类,java.util.ArrayList
·大小可以随意改变
·有很多现成的库函数可以用,需要阅读官方文档来了解更多信息
关于系统类库的使用:
·是否充分理解系统类库的类
·是否充分了解类库中各种函数的功能和使用方法
容器类:
·用来存放任意数量的对象
·有两个类型:
·容器的类型
·元素的类型
2)对象数组:
·对象数组中的每个元素都是对象的管理者而非对象本身
·对象数组的for-each情况与基础数据类型的情况不同:
class Value { private int i = 0; public void set( int i ) { this.i = i; } public int get() { return i; } } public class TestArrayList { public static void main(String[] args) { Value[] a = new Value[10]; for ( int i = 0 ; i < a.length; i++ ) { a[i] = new Value(); a[i].set(i); } for ( Value v : a ) { System.out.println(v.get()); } for ( Value v : a) { v.set(0); } for ( Value v : a ) { System.out.println(v.get()); } } }
则第一次输出是0~9十个数,而第二次输出是十个0。由此可见,对象数组的for-each可以对数组内容进行修改。
·对于容器类来说,for-each循环也可以用
3)集合容器:Set
·集合的概念与数学中的集合完全一致:
·集合中的每个元素都是不重复的,即每个元素具有唯一的值
·集合中的元素没有顺序
·集合也是一种泛型类:HashSet<String> s = new HashSet<String>();
HashSet和ArrayList的对象都可以用System.out.println()直接输出,输出结果为[]中显示元素值,其中ArraryList的值是顺序的,而Set中是无序的。
toString():
任何一个Java类,只要在其中实现:
public String toString()
{
return <String类型字符串>;
}
就可以直接写成System.out.println( <该类的对象名> );
这种写法将会使System.out直接调用该类中的toString(),并输出toString()返回的相应结果(String)。
如果类中没有toString(),System.out.println( <该类的对象名> );则会输出对象变量的值,即对象实体的引用,类似输出指针会输出指针指向的地址,格式为<类的全名>@<十六进制数>。
4)散列表
HashMap<Integer, String> a = new HashMap<Integer, String>();
数据结构中的散列表里所有的东西都是以一对值的形式存放的:Key键 Value值
关于Integer:容器中的类型必须是对象而不能是基本类型的数据,所以对于整型int,应使用其包裹类型Integer。
对于散列表,每个Key一定是唯一的,所以如果对于同一个键多次放入不同值,最后保存的是最后一次放入的值。
HashMap的一些方法:
a.put( <key>, <value> ); // key->value
a.get( <key> ); // return value
a.containsKey( <key> ); // return true / false
a.keySet(); // HashMap a中所有key的HashSet
a.keySet().size(); // HashSet()的size()方法
对HashMap的for-each:
for ( Integer k : a.keySet() ) { // 枚举所有key
String s = a.get(k);
System.out.println(s); // 输出key对应的value
}
4.继承
继承是面向对象语言的重要特征之一,没有继承的语言只能被称作“使用对象的语言”。继承是非常简单而强大的设计思想,它提供了我们代码重用和程序组织的有力工具。基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是public还是private。当然,在这之后,程序员还可以加入自己的新的成员,包括变量和方法。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。继承是支持代码重用的重要手段之一。
对理解继承来说,最重要的事情是,知道哪些东西被继承了,或者说,子类从父类那里得到了什么。答案是:所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法。构造方法是父类所独有的,因为它们的名字就是类的名字,所以父类的构造方法在子类中不存在。除此之外,子类继承得到了父类所有的成员。
但是得到不等于可以随便使用。每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。
public的成员直接成为子类的public的成员,protected的成员也直接成为子类的protected的成员。Java的protected的意思是包内和子类可访问,所以它比缺省的访问属性要宽一些。而对于父类的缺省的未定义访问属性的成员来说,他们是在父类所在的包内可见,如果子类不属于父类的包,那么在子类里面,这些缺省属性的成员和private的成员是一样的:不可见。父类的private的成员在子类里仍然是存在的,只是子类中不能直接访问。我们不可以在子类中重新定义继承得到的成员的访问属性。如果我们试图重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量,在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个。尽管它们同名但是互不影响。
在构造一个子类的对象时,父类的构造方法也是会被调用的,而且父类的构造方法在子类的构造方法之前被调用。在程序运行过程中,子类对象的一部分空间存放的是父类对象。因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化。在这个过程中,如果父类的构造方法需要参数,如何传递参数就很重要了。
子类比父类更加具体。例如鸟是父类,那么啄木鸟,猫头鹰,麻雀,鸵鸟都是鸟的子类。
子类通过继承得到了父类的所有东西,包括private的成员,尽管可能无法直接访问和使用,但继承自父类的东西确实存在,父类中所有public和protected的成员,在子类中都可以直接拿来使用。
关键字:extends
class thisClass extends superclass { // 表明thisClass是superClass 的子类
}
用关键字super表示父类,以区分调用的同名函数是子类中的还是父类中的,就像this用来表示当前对象,以区分同名变量是局部的还是成员的一样。
在创建一个子类的对象时的代码执行顺序总是:
1子类的构造器中的super()(如果有super的话)
↓
2父类构造器中的super()(如果有super的话)
↓
3父类成员变量定义初始化(如果有初始化的话)
↓
4父类构造器内部
↓
5子类成员变量定义初始化(如果有初始化的话)
↓
6子类构造器内部
也就是说,在构造一个子类的对象时,父类的构造方法在子类的构造方法之前被调用。
构造器是构造对象时的入口,但并不是最优先被执行的,成员变量定义初始化最优先,若无定义初始化则跳过。
在子类中,无论是否存在super(),在构造子类的对象时都会自动调用父类的构造器:若无super,则默认调用父类无参构造器,此时若父类中存在有参构造器,但没有无参构造器则会报错;若有super(<参数表>),则会根据参数表重载相应的父类构造器。
5.多态变量与造型
对象变量是多态变量,可以保存其声明类型的对象,或该类型的任何子类型的对象。
子类和子类型
·类定义了类型
·子类定义了子类型
·子类的对象可以被当作父类的对象来使用
·赋值给父类的变量
·传递给需要父类对象的函数
·放进存放父类对象的容器
子类的对象可以赋值给父类的对象变量。
如Vehicle类是父类,其子类有Car,Bicycle等,那么Bicycle类和Car类的对象都可以赋值给Vehicle类的对象变量:
Vehicle v = new Vehicle();
Vehicle v = new Car();
Vehicle v = new Bicycle();
子类和参数传递:
子类的对象可以传递给需要父类对象的函数。
子类的对象可以放在存放父类对象的容器里。
多态变量
·Java的对象变量是多态的,它们能保存不止一种类型的对象
·它们可以保存的是声明类型的变量,或声明类型的子类的对象
·当把子类的对象赋值给父类的变量时,就发生了向上造型
静态类型与动态类型
·一个对象变量的声明类型就是其静态类型
·一个对象变量当前指向的对象的类型是动态类型
因此Java中的对象变量总是有两个类型,一个是声明类型,一个是动态类型,只是有时这两个类型可能是一致的。
造型cast
·一个类型的对象赋值给另一个类型的对象变量的过程
·子类的对象可以赋值给父类的变量
·注意!Java中不存在对象对对象的赋值
·父类的对象不能赋值给子类的变量
·当父类的变量当前指向的是子类的对象时,可以用造型来赋值,否则会出现异常:ClassCastException
如,Vehicle是父类Car是子类:
Vehicle v;
Car c = new Car();
v = c; // 可以
c = v; // 编译错误
c = (Car)v; // 造型,此时v指向的是Car的对象,即v的动态类型是Car,可以用造型来赋值给Car类型的变量
再如,Item是父类,CD是子类:
Item item = new Item();
CD cd = new CD();
item = cd; // 可以
CD cc = (CD)item; // 造型,如果没有(CD)则报错
造型
·用括号围起类型放在值的前面
·对象本身并没有发生任何变化
·所以不是“类型转换”:类型转换是将原来的值变成转换类型的值之后赋值,而造型是将当前类型的值(对象)看作是造型类型的值来看待,并没有修改替换成为另外类型的值
·运行时有机制来检查这样的转化是否合理
·ClassCastException
向上造型:把子类的对象赋值给父类的变量,(<父类型>)可以省略
·拿一个子类的对象,当作父类的对象来用
·向上造型是默认的,不需要运算符
·向上造型总是安全的
向下造型:把子类的对象通过父类变量赋值给子类变量,即父类变量当前指向子类对象,需要用到(<子类型>)
6.多态
如果子类的方法覆盖了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。覆盖的新版本具有与老版本相同的方法签名:相同的方法名称和参数表。因此,对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。
覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被“覆盖”起来而看不见了。而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。注意我们这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。
当调用一个方法时,究竟应该调用哪个方法,这件事情叫做绑定。绑定表明了调用一个方法的时候,我们使用的是哪个方法。绑定有两种:一种是早绑定,又称静态绑定,这种绑定在编译的时候就确定了;另一种是晚绑定,即动态绑定。动态绑定在运行的时候根据变量当时实际所指的对象的类型动态决定调用的方法。Java缺省使用动态绑定。
函数调用的绑定
·当通过对象变量调用函数的时候,调用哪个函数这件事情可以叫做绑定
·静态绑定:根据变量的声明类型来决定
·动态绑定:根据变量的动态类型来决定
·在成员函数中调用其它成员函数也是通过this这个对象变量来调用的
覆盖/重写override
·子类和父类中存在名称和参数表完全相同的函数,这一对函数构成覆盖关系
·通过父类的变量调用存在覆盖关系的函数时,会调用变量当时所管理的对象所属类的函数
所谓的多态就是,通过一个对象变量调用一个函数时,并不去判断对象变量的动态类型可能是什么,当程序执行的时候,让其自动通过当前动态类型来执行相应的函数。
在函数重写时,函数的前一句话应该是@Override,子类中的重写函数的声明部分(即函数头)应与父类的完全一致,包括限定词,返回值类型,函数名,参数表
如:
@Override
public String toString()
{
return <String>;
}
@Override的作用是,告诉编译器接下来的函数是对父类同名函数的重写(即覆盖),如果接下来的函数与父类同名函数有任何不一致的地方,则会报错;如果没有@Override,则不会对接下来的函数做任何限制,同样地也就不再必须是对父类函数的重写。
Java类型系统的根:Object类
Java是一种单根结构的语言,Java中的所有类默认看作是Object类的子类,即,所有的类都是继承自Object的。
Object类的函数
·toString()
·equals()
·可通过eclipse补全功能查看其他函数
可扩展性:代码不需要经过修改就可以适应新的内容或数据
可维护性:代码经过修改可以适应新的内容或数据
7.设计原则
1)消除代码复制:
程序中存在相似甚至相同的代码块,是非常低级的代码质量问题。
代码复制存在的问题是,如果需要修改一个副本,那么就必须同时修改所有其他的副本,否则就存在不一致的问题。这增加了维护程序员的工作量,而且存在造成错误的潜在危险。很可能发生的一种情况是,维护程序员看到一个副本被修改好了,就以为所有要修改的地方都已经改好了。因为没有任何明显迹象可以表明另外还有一份一样的副本代码存在,所以很可能会遗漏还没被修改的地方。
消除代码复制的两个基本手段,就是函数和父类。
2)用封装来降低耦合
·类和类之间的关系称作耦合
·耦合越低越好,保持距离是形成良好代码的关键
要评判某些设计比其他的设计优秀,就得定义一些在类的设计中重要的术语,以用来讨论设计的优劣。对于类的设计来说,有两个核心术语:耦合和聚合。耦合这个词指的是类和类之间的联系。之前的章节中提到过,程序设计的目标是一系列通过定义明确的接口通信来协同工作的类。耦合度反映了这些类联系的紧密度。我们努力要获得低的耦合度,或者叫作松耦合(loose coupling)。
耦合度决定修改应用程序的容易程度。在一个紧耦合的结构中,对一个类的修改也会导致对其他一些类的修改。这是要努力避免的,否则,一点小小的改变就可能使整个应用程序发生改变。另外,要想找到所有需要修改的地方,并一一加以修改,却是一件既困难又费时的事情。另一方面,在一个松耦合的系统中,常常可以修改一个类,但同时不会修改其他类,而且整个程序还可以正常运作。
聚合与程序中一个单独的单元所承担的任务的数量和种类相对应有关,它是针对类或方法这样大小的程序单元而言的理想情况下,一个代码单元应该负责一个聚合的任务(也就是说,一个任务可以被看作是一个逻辑单元)。一个方法应该实现一个逻辑操作,而一个类应该代表一定类型的实体。聚合理论背后的要点是重用:如果一个方法或类是只负责一件定义明确的事情,那么就很有可能在另外不同的上下文环境中使用。遵循这个理论的一个额外的好处是,当程序某部分的代码需要改变时,在某个代码单元中很可能会找到所有需要改变的相关代码段。
3)增加可扩展性:
·可以运行的代码 != 良好的代码
·对代码做维护的时候最能看出代码的质量
·可扩展性的意思就是代码的某些部分不需要经过修改就能适应将来可能的变化
·用接口来实现聚合
·在类的内部实现新的方法,把具体细节彻底隐藏在类的内部
·今后某一功能如何实现就和外部无关了
·用容器来实现灵活性
·减少“硬编码”
·对于随时变化数量的同类事物,用容器来实现,比如HashMap
·框架+数据提高可扩展性
·从程序中识别出框架和数据,以代码实现框架,将部分功能以数据的方式加载,这样能在很大程度上实现可扩展性。
8.抽象
我们用abstract关键字来定义抽象类。抽象类的作用仅仅是表达接口,而不是具体的实现细节。抽象类中可以存在抽象方法。抽象方法也是使用abstract关键字来修饰。抽象的方法是不完全的,它只是一个方法签名而完全没有方法体。
如果一个类有了一个抽象的方法,这个类就必须声明为抽象类。如果父类是抽象类,那么子类必须覆盖所有在父类中的抽象方法,否则子类也成为一个抽象类。一个抽象类可以没有任何抽象方法,所有的方法都有方法体,但是整个类是抽象的。设计这样的抽象类主要是为了防止制造它的对象出来。
抽象abstract
·抽象函数:表达概念而无法实现具体代码的函数
·带有abstract修饰的函数
·public abstract void method(); // 必须结尾带分号,而不能是空的大括号{},就像C中的函数声明
·抽象类:表达概念而无法构造出实体的类
·有抽象函数的类一定是抽象类
·抽象类不能制造对象
·但是可以定义变量:任何继承了该抽象类的非抽象子类的对象可以赋给这个抽象类的变量,即由抽象类变量管理其非抽象子类的对象。
实现抽象函数
·继承自抽象类的子类必须覆盖所有父类中的抽象函数,这里的对父类的抽象函数的覆盖称作实现implement
·否则子类也是抽象类。因为有继承自父类的抽象函数未重写,只要一个类中有抽象函数,这个类就必须是抽象类。
写法:
public abstract class Shape {
public abstract void draw(Graphics g); // 只有函数头加;没有函数体
}
public class Line extends Shape { // Line是继承自抽象类Shape的非抽象子类
… …
@Override
public void draw(Graphics g) //对抽象类Shape的抽象方法draw的具体实现
{
… …
}
}
两种抽象
·与具体相对:表示一种概念而非实体
·与细节相对:表示在一定程度上忽略细节而着眼大局
数据与表现分离
·程序的业务逻辑与表现无关
·表现可以是图形的也可以是文本的
·表现可以是当地的也可以是远程的
·负责表现的类只管根据数据来表现
·负责数据的类只管数据的存放
·一旦数据更新以后,表现仍然可以不变
·不用去精心设计哪个局部需要更新
·简化了程序逻辑
·这是在计算机运算速度提高的基础上实现的
责任驱动的设计
·将程序要实现的功能分摊到适合的类/对象中去是设计中非常重要的一环
网格化
·图形界面本身有更高的解析度
·但是将画面网格化以后,数据就更容易处理了
9.接口
接口interface
·类表达的是对某类东西的描述,而接口表达的是抽象的概念和规范
·接口是纯抽象类
·所有的成员函数都是抽象函数
·所有的成员变量都是public static final
·因此不需特意声明abstract
·接口规定了长什么样,但不管里面有什么东西
·在Java中,接口就是一种特殊的class
·它与class地位相同
·接口类型的变量可以被赋值任何实现了这个接口的类的对象
写法:
public interface Cell {
int size; // public static final
void draw( … ); // abstract
}
实现接口
·类用extends表达子类继承父类,用implements表达一个类实现了一个接口
·一个类可以实现很多接口
·接口可以继承接口,但不能继承类
·接口不能实现接口
写法:
public class Fox extends Animal implements Cell {
… …
@Override
public void draw()
{
… …
}
}
面向接口的编程方式
·设计程序时先定义接口,再实现类
·任何需要在函数间传入传出的一定是接口而不是具体的类
·是Java的成功关键之一,因为极适合多人同时写一个大程序
·也是Java被批评的要点之一,因为代码量会迅速膨胀,整体显得臃肿
10.控制反转与MVC设计模式
内部类就是指一个类定义在另一个类的内部,从而成为外部类的一个成员。因此一个类中可以有成员变量、方法,还可以有内部类。实际上Java的内部类可以被称为成员类,内部类实际上是它所在类的成员。所以内部类也就具有和成员变量、成员方法相同的性质。比如,成员方法可以访问私有变量,那么成员类也可以访问私有变量了。也就是说,成员类中的成员方法都可以访问成员类所在类的私有变量。内部类最重要的特点就是能够访问外部类的所有成员。
内部类
·定义在别的类的内部或函数内部的类
·内部类能直接访问外部的全部资源
·包括任何私有成员,因为内部类也是类的成员之一
·外部是函数时,只能访问那个函数里的final的变量
匿名类
·在new对象的时候给出的类的定义形成了匿名类
·匿名类一定是内部类,可以继承某类,也可以实现某接口
·Swing的消息机制广泛使用匿名类
写法:
new 类名/接口名() {
… …
}
如:
JButton butStep = new JButton(“OneStep”);
butStep.addActionListener(new ActionListener() {
// addActionListener()是JButton类的方法,ActionListener是JButton的接口
@Override
public void actionPerformed()
{
……
}
}); // 定义匿名类,实现ActionListener接口
注入反转·控制反转
·由按钮(别人写的类)公布一个守听者接口和一对注册/注销方法
·根据需要编写代码实现那个接口,将守听者对象注册在按钮上
·一旦按钮被按下,就会反过来调用你的守听者对象的某个方法
MVC
·数据、表现和控制三者分离,各负其责
·M = Model(模型)
·V = View(表现)
·C = Control(控制)
·模型:保存和维护数据,提供接口让外部修改数据,通知表现需要刷新
·表现:从模型获得数据,根据数据画出表现
·控制:从用户得到输入,根据输入调整数据
·在MVC中,很重要的一件事是,Control和View之间没有任何联系。用户在界面上所做的操作(控制)不直接修改界面上的显示(表现),即,用来接收用户操作/输入的代码并不直接修改在屏幕上显示的内容,而是用户的输入来调整内部的数据,再由内部数据去触发表现刷新。
·这样做的好处是,每一部分都很单纯,不需要去考虑其他细节的东西。
11.异常
捕捉异常
Try {
// 可能产生异常的代码
} catch(Type1 id1) {
// 处理Type1异常的代码
} catch(Type2 id2) {
// 处理Type2异常的代码
} catch(Type3 id3) {
// 处理Type3异常的代码
}
·得到异常对象之后
·String getMessage();
·String toString();
·void printStackTrace();
·并不能回到发生异常的地方,具体的处理逻辑取决于业务逻辑需要
再度抛出
catch(Exception e) {
System.err.println(“An exception was thrown”);
throw e;
}
·如果在这个层面上需要处理,但并不能做最终的决定,就catch之后再throw
读取文件使用异常机制的例子(伪码描述):
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch ( fileOpenFailed ) {
doSomething;
} catch ( sizeDeterminationFailed ) {
doSomething;
} catch ( memoryAllocationFailed ) {
doSomething;
} catch ( readFailed ) {
doSomething;
} catch ( fileClosedFailed ) {
doSomething;
}
异常
·有不寻常的事情发生了
·当这个事情发生的时候,原本打算要接着做的事情不能再继续了,必须得要停下来,让其他地方的某段代码来处理
·异常机制最大的好处是清晰地分开了正常业务逻辑代码和遇到情况时的处理代码
异常声明
·如果函数可能抛出异常,就必须在函数头部加以声明
void f() throws TooBig, TooSmall, DivZero { … }
·可以声明并不会真的抛出的异常,以使函数更加灵活,增加可扩展性,方便以后的补充修改
可以抛出的
·任何继承了Throwable类的对象
·Exception类继承了Throwable
·throw new Exception();
·throw new Exception(“HELP”);
catch匹配异常
·Is-A的关系
·抛出的子类异常会被捕捉父类异常的catch捕捉到
捕捉任何异常
catch(Exception e) {
System.err.println(“Caught an exception”);
}
运行时刻异常
·像ArrayIndexOutOfBoundsException这样的异常是不需要声明的
·但是如果没有适当的机制来捕捉,就会最终导致程序终止
异常声明遇到继承关系
·当覆盖一个函数的时候,子类不能声明抛出比父类版本更多的异常
·在子类的构造函数中,必须声明父类可能抛出的全部异常,之后还可以增加子类自己的异常声明
12.流
流的基础类
·InputStream
·OutputStream
·都是对字节进行操作:字节流
InputStream
·read()
·int read()
·read(byte b[])
·read(byte[], int off, int len)
·skip(long n)
·int available()
·mark()
·reset()
·boolean markSupported()
·close()
OutputStream
·write()
·write(int b)
·write(byte b[])
·write(byte b[], int off, int len)
·flush()
·close()
文件流
·FileInputStream
·FileOutputStream
·对文件作读写操作,也是字节
·实际工程中已经较少使用
·更常用的是以在内存数据或通信数据上建立的流,如数据库的二进制数据读写或网络端口通信
·具体的文件读写往往有更专业的类,比如配置文件和日志文件
流过滤器
·以一个介质流对象为基础层层构建过滤器流,最终形成的流对象能在数据的输入输出过程中,逐层使用过滤器流的方法来读写数据
Data
·DataInputStream
·DataOutputStream
·用以读写二进制方式表达的基本数据类型的数据
文本流
Reader/Writer
·二进制数据采用InputStream/OutputStream
·文本数据采用Reader/Writer
在流上建立文本处理
PrintWriter pw == new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(“abc.txt”))));
Reader
·常用的是BufferedReader
·readLine()
LineNumberReader
·可以得到行号
·getLineNumber()
FileReader
·InputStreamReader类的子类,所有方法都从父类中继承而来
·FileReader(File file)
·在给定从中读取数据的File的情况下创建一个新FileReader
·FileReader(String fileName)
·在给定从中读取数据的文件名的情况下创建一个新FileReader
·FileReader不能指定编码转换方式
汉字编码
·InputStreamReader(InputStream in)
创建一个使用默认字符集的InputStreamReader
·InputStreamReader(InputStream in, Charset cs)
创建使用给定字符集的InputStreamReader
·InputStreamReader(InputStream in, CharsetDecoder dec)
创建使用给定字符集解码器的InputStreamReader
·InputStreamReader(InputStream in, String charsetName)
创建使用指定字符集的InputStreamReader
格式化输入输出
·PrintWriter
·format();
·printf();用法与C一致
·print();
·println();
·Scanner
·在InputStream或Reader上建立一个Scanner对象可以从流中的文本中解析出以文本表达的各种基本类型
·next…();
Stream/Reader/Scanner
数据是二进制的
是->用InputStream
否->表达的是文本
是->用Reader
否->用Scanner
阻塞/非阻塞
·read()函数是阻塞的,在读到所需的内容之前会停下来等
·使用read()的更“高级”的函数,如newInt()、readLine()都是这样的
·所以常用单独的线程来做socket读的等待,或使用nio的channel选择机制
·对于socket,可以设置SO时间
·setSoTimeout(int timeOut)
对象串行化
·ObjectInputStream类
·readObject()
·ObjectOutputStream类
·writeObjeact()
·Serializable接口