想写出更加优美的代码,所以打算花时间学习《think in java》以及编程模式等基础知识。
对象以及面向对象编程
所有的编程语言的最终目的都是提供一种抽象方法。
Light lt = new Light();
lt.on();
这个例子中类型/类名是Light,为Light申明一个lt句柄,再用new关键字新建类型为Light的一个对象,再用“=”将其复制给句柄。在为对象发送一条消息(on),用“.”将消息和句柄连接起来
java决定了每种主要类型(基本类型)的大小,这些数据的大小不会随着机器结构的变化尔变化,这也是Java程序有很强的移植能力原因之一
两个用于高精度计算的两个类,BigInteger和BigDecimal,前面可以用于计算任意精度的整数,后面可以用于计算任意精度的定点数。
定点数和浮点数的区别
Java对象不具备与主类型一样的存在时间,用new关键创建Java对象的时候,它会超出作用域的范围之外
{
String s = new String("reee");
}
句柄s会在作用域的终点消失,然而s指向的string对象依然占据着内存空间。但是我们无法访问对象,因为指向它的唯一一个句柄已经超出了作用域
上面阐述的情况在c c++ Java语言中常出现,常c++程序是程序员来释放那部分的内存,Java中则选择用GC来回收那部分内存
一旦将变量作为成员使用,就要特别注意由java分配的默认值。这样可以保证主类型的成员变量肯定得到了初始化,可以有效遏制很多相关的编程错误
int i;
这里i会得到一些随机值(ps,书上说事随机值,但是我得到的却都是零)
java 的方法决定了一个对象能够接受到的消息
自变量列表(方法参数列表)
自变量列表规定了我们传递给方法的什么信息,我们实际传递的句柄,句柄的类型必须正确
对于那些基本类型boolean char byte short int long float以及double传递是直接传递值,不是传递的句柄,传递对象的时候通常指的事传递指向对向的句柄
java传递的都是值传递,传递的是 栈上的句柄
初始化和清除
c++引入构建器概念,用于初始化一个数据结构,java引用垃圾收集器,当资源不再需要的时候自动释放他们
当用new 关键字创建一个对象的时候,就会调用构建器,构建器的名字必须和类名相同,构建器是一种比较特殊的方法类型,因为他没有返回值,这个与void是有区别的,对于void返回值,尽管方法本身是不会有自动返回什么,但是任然可以让他返回另外一些东西,构建器不同,它不经什么也不会返回也不会自动返回什么
public TestBean() {
// TODO Auto-generated constructor stub
System.out.println("------test 构建-----");
}
public void TestBean() {
// TODO Auto-generated constructor stub
System.out.println("------test 构建-----");
}
实际上在创建对象时 下面的那个根本不会打印出任何东西,也就是说他不会被调用。
方法过载(重载 Overloading)
日常生活中我们用相同的词表达不同的含义,即过载。
java中区分过载方法,每个过载的方法都必须采用独一无二的自变量类型列表
关于主变量过载,主类型能够从一个较小的类型 自动转变成为一个较大的类型,在过载的时候就会有一些问题。
若我们的数据类型小于 方法中使用的自变量,就会对那种数据类型进行转型处理,cahr获得的效果稍有些不同,如果他没有发现一个准确的char,就会转型为int,转型成为int后就调用自变量是int类型的方法
TestBean2 t2 = new TestBean2();
int a=2;
t2.overTest(a);
public void overTest(byte a) {
System.out.println("--------byte int--------"+a);
}
如果方法采用了容量更小,范围更窄的主类型值,就必须使用括号强制转换,不这样的货编译器会报告错误
这种缩小转换,在强制转型过程中可能会丢失一些信息,编译器强迫我们明确定义的原因——我们需要明确表达想要转型的愿望
为什么不利用返回值却别过载
当我们调用一个方法是,同事忽略返回值,通常称为“为它的副作用去调用一个方法”,此刻我们不关心返回值,只是就无法判断具体是调用的哪一个方法。所以无法用返回值来区分过载
this 关键字
this 关键字可以为已调用了其方法的那个对象生成相应的句柄,只能够在方法内部使用,对象可对待他的任何句柄一样对待这个句柄
class Apricot{
void pick(){}
void pit(){
pick();
}
}
在pit内部,调用pick(),实际上是this.pick(),凡是我们无需这样,编译器会帮我自动完成。
this关键字只能用于那些特殊的类——需明确使用当前对象的句柄。这样可以实现一些链式操作
this关键字可以调用一个构建器,但是不能够同时调用两个构建器
public TestBean2(int a){
this(12,"re");
}
public TestBean2(int a,String b) {
this(12);
}
这样java 编译器就会报错
static 关键字
static关键字意味着一个特定的地方没有this,我们不可以从一个static方法中调用一个非static方法(原因是,分配内存的时机不同),返回来却是可以的(非static方法可以调用static方法)
static分配内存是在加载类的时候分配,而非static则是在运行申明时分配内存
我们可以在没有任何对象的前提下,可以针对类本身发出一个static方法调用,这是static方法的正真意义
垃圾收集器 只知道释放那些由new分配的内存,所以不知道如何释放对象的特殊内存,为此java提供了一名为finalize()的方法,理想的工作原理是,一旦垃圾收集器准备好释放对象占用的储存空间,它首先调用fianlize(),而且只有在下一次 垃圾搜集过程中,才会真正回收内存的对象,这时可以在finalize()中进行一些重要的清除或清扫工作。
c++中的破坏器使用的fianlize清除一个对象的时候,肯定会低啊用这个对象,而且也肯定会被清除,而java调用这个对象,也不一定会被回收
垃圾收集并不等于破坏
因为上面的原因,所以当我们不在需要某个对象的时候,有些操作是必须由自己来完成的,例如,假设在创建对象的过程中,将会将自己绘画到屏幕上,如果不从屏幕上明确的删除他的图像,那他可能永远不会被删除
我们的对象可能不会当做垃圾被回收掉
有可能发现一个对象的储存空间永远都不会被释放掉,因为自己的程序永远都接近于用光空间的临界点。有时这是一件好事,应为垃圾收集本身也要消耗一些开销,如果永远不用他,那么永远也不用支出这部分开销
System.runFinalization()
强制调用已经失去引用对象的finalize方法
System.gc()
告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行是不确定的
初始化
java中一个类的所有基本数据类型成员都会保证获得一个初始值。
boolean的初始值是false char 是null(打印不会打印任何东西),其他的则是0
class clnit{
int j=g(i);
int i=f();
}
class clnit{
int i=f();
int j=g(i);
}
上面的做法是违法的,调用前必须初始化。编译器对向前引用感到不适应的原因,因为它与它的初始化顺序有关,而不是与程序编译方式有关。
在类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布与方法定义中间,那些变量任会在调用任何方法之前得到初始化。
static初始化只在有必要的时候才会进行。
对象创建过程:
1. Java解释器找到类
2. 找到类后穿件一个对象,此时所有的static初始化模块将会在此时运行,因此static初始化仅发生一次,在clas对象首次载入的时候
3. 创建一个对象时,对象的构建进程会在内存堆里为一个对象分配足够多的储存空间。
4. 这种储存空间会清为零,将对象中的所有基本数据类型设为他们的默认值。
5. 进行字段定义时发生生的所有初始化都会执行
6. 执行构建器。
明确进行的静态初始化
Java允许我们将他们的static初始化工作划分到类内一个特殊的static构建从句,有时也叫作静态块里
class spoon{
static int i;
static{
i=47;
}
}
这里的静态块看起来象个方法,但它实际上只是一个static关键字,他后面跟随一个方法主体,这段代码也只执行一次,首次生成那个类的对象时,或者首次访问那个类的一个static成员时(即使从未生成那个类的对象)
数组初始化
数组代表一系列对象或者基本数据类型,所有相同的类型封装到一起,采用统一的标识符。
int[] al;
int al[];
两种命名方式都可以,后一种是c的传统方式,前一种更符合Java习惯
所有的数组都有一个本质的成员(无论他们的对象是数组还是基本数据类型),可以对其进行查询,但是不能够改变,从而获取数组内包含好多元素。这个成员是length。与c和c++类似,由于Java数组从元素0开始计数,所以索引最大元素编号是length-1,如果超出边界c和c++会默默的接受,并且允许胡乱使用内存。但是Java会报出一超出数组边界异常。
数组的创建实际是在运行期间进行的。
隐藏实施过程
面向对象设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔
java访问指示符
友好的(friendly)
如果根本不指定访问指示符,这意味着当前包内的其他所有类都能够访问“友好的”成员,
一个继承的类既可以访问一个protected成员,也可以访问一个public成员。只有在两个类位于相同包内时,其他类就可以访问成员
提供 访问其(getter)、变化器(setter),y以便读取和修改值,是OOP环境中最正规的一种方法,也是java beans的基础
public接口访问
使用public关键字时,意味着紧随在public后面的成员申明使用于所有人,特别是适用于用库的客户程序员。
不要错误的意味java无论如何都会将当前目录作为搜索的起点看待,如果不将.作为classpath的一部分,java就不会考虑当前目录(不懂 这句话,只是摘抄下来)
在同一目录中,没有明确的报名,java把象这样的文件看做那个目录 默认包的一部分,所以他们对于目录的其他文件来说是友好的
private 不能接触的
private意味着除非那个特定的类,而且从那个类的方法里,否则没有人能访问那个成员。同一个包内的其他成员不能访问private成员,这使其显得视乎将类与我们自己隔离起来
默认构建器是唯一获得定义的,而且他的属性是private,所以可以防止这个类的继承
protected 友好的一种
protected 访问指示符,是以继承为基础的,若新建一个包,并从另一个包内的某个类里继承,则唯一能够访问的成员就是原来那个包的public成员,当然,如果在相同的包里进行继承,那么继承获得的包能够访问所有友好的成员,有的时候,基础类的创建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是protected的工作
接口与实现
将数据和方法封装到类内后,可生成一种数据类型,它具有自己的特征与行为。但由于两方面重要的原因,访问为那个数据加上了自己的边界。第一个原因是规定客户程序员那些能够使用,那些不能够使用,我们可在结构里构建自己的内部机制,不用担心客户程序员将其当做接口的一部分,从而自由的使用或者滥用
这个原因直接导致了第二个原因,我们需要将接口通实施细节分离开。若结构在一系列
类的访问
- 每个编译单元(文件)都只能有一个public类。但是可以拥有多个public呢内部类
- public类的名字必须和包含了编译单元的那个文件的名字完全相符,甚至包括它的大小写形式
对于任何关系,最重要的一点都是规定好所有方面都必须遵守的界限和规则
类再生
我们通过创建新类使用代码,但却用不着创建新类,可以直接使用别人已建好并调试好的现成类,两种方法达到这个要求
第一种:在新类里简单的创建原有类的对象,这种方法叫“合成”,因为新类由现有类的对象合并而成。我们只是简单的重复利用代码的功能,而不采用他的形式
第二种:创建一个新类,将其作为现有类的一个“类型”,我们可以原样采用现有类的形式,并在其加入新代码,同事不会对现在的类产生影响,这种方式叫“继承”
对于合成我们只需在新类里简单地置入对象句柄即可
编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销,如希望句柄得到初始化,可在下面这些地方进行
下面是对象初始化的三种时机
- 在对象定义的时候。这以为着他们在构建器调用之前肯定能得到初始化
- 在那个类的构建器中
- 紧靠在要求实际使用那个对象之前,这样做可减少不必要的开销——加入对象并不要创建的话
累积开发
程序开发时一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据要求进行尽可能的分析,但是在一个项目的设计之初,谁都不可能提前获知所有答案。如果能够将自己的项目看作一个有机的,能不断进步的生物,从而不断发展和改进他,就有望获得更大的成功以及更直接的反馈
继承是一种非常有用的技术,但在项目稳定下来以后,任需要从新的角度考察自己的类结构,将其收缩成为一个更灵活的结构,请记住,继承是对一种特殊关系的表达,意味着“这个新类属于旧类的一种类型”。
上溯造型
上溯造型:将子类的句柄赋值到父类的句柄上
eg:
public class StuOne {
private void syhi() {
System.out.println("say hi");
}
protected void syhi2() {
System.out.println("say hi2");
}
public void sayClassName(StuOne stuone) {
System.out.println(stuone.getClass().getName());
}
}
public class ChildStuOne extends StuOne{
@Override
protected void syhi2() {
// TODO Auto-generated method stub
super.syhi2();
System.out.println("this is child sya hi2");
}
}
public void sayHi() {
StuOne stuOne = new ChildStuOne();
StuOne stuone = new StuOne();
ChildStuOne chidStuOne = new ChildStuOne();
stuOne.syhi2();
stuOne.sayClassName(stuOne);
stuOne.sayClassName(stuone);
stuOne.sayClassName(chidStuOne);
}
输出结果
say hi2
this is child sya hi2
com.tang.stu.ChildStuOne
com.tang.stu.StuOne
com.tang.stu.ChildStuOne
以上是上溯造型例子,说明造型的的类是new的类型决定的
继承最值得注意的地方就是它没有为新类提供方法,继承是对新类和基础之间关系的一种表达,可以这样总结:“新类属于现有类的一种类型”
继承是直接由语言提供支持,由于继承意味着基础类的所有方法亦可在衍生出来的类中使用,所以我们发送给积累的任何消息亦可发送给衍生类
类继承图的画法是根位于最顶部,再向下逐渐扩展。由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型”。上溯造型肯定是安全的,因为我们从一个更特殊的类型转到一个更常规的类型,换言之,衍生类是基础类的一个超集。他可以包含比基础类更多的方法,但是它至少包含了基础类的方法,在上溯造型的时候,类接口可能出现的唯一一个问题是他可能出现丢失方法,而不是赢得这些方法(上溯造型的时候只方法只可能少,不可能多),这便是在没有任何明确的造型或者其他特殊的标注的情况下,编译器为什么允许上溯造型的原因所在。
在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是,将数据和方法统一封装到一个类里,并且使用给那个类的对象。有些时候,需要通过合成现成的类来构造新类。而继承是最少建的一种做法,继承的使用应该特别慎重,只有清楚知道继承在所有方法中最有效的前提下,才可以考虑他。判断是该继承还是该合成,最简单的办法是考虑是否必要重新类上溯到造型回基础类,如果必须要则就需要继承,但是如果不需要上溯造型,就要提醒自己不能够滥用继承。
final 关键字
final关键字的含义可能会稍微产生一些差异。但他最一般的意思就是生命“这个东西不能改变”。之所以要考虑禁止改变,可能是考虑到两方面的因素:设计或效率
final关键字的三种应用场合:数据、方法及类
常数主要应用于两个方面:
1. 编译期常数,它永远不会改变
2. 在运行期初始化的一个值,我们不希望它发生改变
对于编译期常数,编译器可将常数值封装到需要的计算过程里,也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在java中,这些形式的常数必须属于基本数据类型,而且好用final关键字表达,而且要用final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值
java final的基本数据类型 是常数
无论static还是final字段,都只能储存一个数据,而且不得改变。
当final修饰一个句柄的时候,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。也就是在栈上的内存初始的时候必须给他赋值,而且赋值后就不能够再改变了。也就是句柄就不能够再指向另一个对象了。然而本身还是可以修改的。java对此未提供任何手段,可将一个对象直接变成一个常数,这一限制也可适用于数据
空白final,一些声明成final,但是却未得到一个初始值。空白final都必须在实际使用前得到正确的初始化。而且编译器会主动保证这一规定。
final方法
之所以要使用final方法,可能是出于两方面的考虑
一、为方法“上锁”,为防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法
二、采用final方法的第二个理由是程序执行的效率。将一个方法设成final后,编译器就可以把对那个方法的所有调用都植入“嵌入”调用里。编译器发现一个final方法,就会忽略为执行方法调用机制而采用常规代码插入方法(将自变量压入堆栈,跳至方法代码并执行它,跳回来,清除堆栈自变量,最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做避免方法调用时的系统开销。当然方法体积过大,那么程序也会变得臃肿。java编译器能够自动侦测这些情况,并颇为明智的决定是否嵌入一个final方法,然而最好还是不要完全相信编译器能够正确的做出所有的判断。通常只有在方法的代码非常少,或者是明确禁止方法覆盖的时候,才应该考虑将一个方法设置final
final 方法在执行的时候 所有的方法调用都用实际的方法代码所代替
类内的所有private方法都自动成为final,由于我们不能访问一个private方法,所以他绝对不会被其他方法覆盖,可以为一个private方法添加final指示符,但是不能为那个方法提供任何额外的含义
final类
如果整个类都是final,就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。换言之,出于这样或者那样的原因,我们的类肯定不需要进行任何变化,或者出于安全的理由,我们不希望进行子类化
将类定义成final后,结果只是禁止进行继承,然而由于它禁止了继承,所以一个final类中的所有方法都默认为final。因为此时再也无法覆盖他们。
可以为final类里的一个方法添加final指示符,但是这样做没有任何意义
设计一个类的时候,往往需要考虑是否将一个方法设为final。可能会觉得使用自己的类时执行效率非常重要,没有人想覆盖自己的方法,这种想法在某些时候是正确的。但是应该慎重的做出这种决定,因为我们无法预测程序接下来的发展。
标准Java库是阐述这一观点的最好例子。其中特别常用的一个类是Vector。如果我们考虑代
码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易
就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们
至少可以用两个理由来反驳他们。首先,Stack(堆栈) 是从Vector继承来的,亦即
Stack“是”一个Vector,这种说法是不确切的。其次,对于Vector许多重要的方法,如
addElement()以及elementAt()等,它们都变成了synchronized(同步的) 。正如在第14章要
讲到的那样,这会造成显著的性能开销,可能会把final提供的性能改善抵销得一干二净。因
此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,
真不敢想象会在程序员里引发什么样的情绪。
这是《think in java》的原话但是不是很能理解,所以在这里做了标注
初始化和类装载
传统语言里,程序都是作为启动过程的一部分一次性载入的,随后进行的事初始化,再是进行执行程序。在这些语言里,必须对初始化过程进行慎重的控制。
java没有这样的问题,因为他采用了不同的装载方法,由于java一切都是对象,每个对象的代码都存在独立的文件中,除非真的需要代码,否则那个文件是不会载入的,通常我们可认为除非那个类的一个对象构造完毕,否则代码不会真的载入。
无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我
们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使
用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接
口,所以能够将其“上溯造型”为基础类。