Java语言中接口优于抽象类
Java语言设计提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。
因为Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。现有的类可以很容易被更新,以实现新的接口。一般来说,无法更新现有的类来
扩展新的抽象类。如果你希望两个类来扩展一个抽象类,就必须把抽象类放到类型层次的高处,以便这两个类的一个祖先成为他的子类。这样会间接的
伤害到类的层次,迫使这个公共祖先的所有后代类都扩展这个新的抽象类,无论他对于这个后代类是否合适。
接口是定义mixin(混合类型)的理想选择。类除了实现他的“基本类型(primary type)”之外,还可以实现这个mixin类型,以表示提供了某些可供
选择的行为。
接口允许我们构造非层次结构的类型框架。包装类(wrapper class)模式,接口使得安全地增强类的功能成为可能。
通过对你导出的每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,把这个抽象类的优点结合起来。
必须认真研究接口,并确定哪些方法是最为基本的(primitive),其他的方法则可以根据他们来实现。这些基本的方法将成为骨架实现类中抽象方法
。然后,必须为接口中所有其他的方法提供具体的实现。
抽象类的演变比接口的演变要容易的多。接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。
java的动态绑定
所谓的动态绑定就是指程执行期间(而不是在编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
java继承体系中的覆盖就是动态绑定的,看一下如下的代码:
class Father {
public void method(){
System.out.println("This is Father's method");
}
}
class Son1 extends Father{
public void method(){
System.out.println("This is Son1's method");
}
}
class Son2 extends Father{
public void method(){
System.out.println("This is Son2's method");
}
}
public class Test {
public static void main(String[] args){
Father s1 = new Son1();
s1.method();
Father s2 = new Son2();
s2.method();
}
}
运行结果如下:
This is Son1's method
This is Son2's method
通过运行结果可以看到,尽管我们引用的类型是Father类型的,但是运行时却是调用的它实际类型(也就是Son1和Son2)的方法,这就是动态
绑定。在java语言中,继承中的覆盖就是是动态绑定的,当我们用父类引用实例化子类时,会根据引用的实际类型调用相应的方法。
java的静态绑定
相对于动态绑定,静态绑定就是指在编译期就已经确定执行哪一个方法。在java中,方法的重载(方法名相同而参数不同)就是静态绑定的,重
载时,执行哪一个方法在编译期就已经确定下来了。看一下代码:
class Father {}
class Son1 extends Father{}
class Son2 extends Father{}
class Execute {
public void method(Father father){
System.out.println("This is Father's method");
}
public void method(Son1 son){
System.out.println("This is Son1's method");
}
public void method(Son2 son){
System.out.println("This is Son2's method");
}
}
public class Test {
public static void main(String[] args){
Father father = new Father();
Father s1 = new Son1();
Father s2 = new Son2();
Execute exe = new Execute();
exe.method(father);
exe.method(s1);
exe.method(s2);
}
}
运行结果如下:
This is Father's method
This is Father's method
This is Father's method
在这里,程序在编译的时候就已经确定使用method(Father father)方法了,不管我们在运行的时候传入的实际类型是什么,它永远都只会执行
method(Father father)这个方法。也就是说,java的重载是静态绑定的。
Java面向对象设计 构造函数设计
在Javascript面向对象设计一——我们将使用构造函数模式将工厂模式进行改写。
function Employee(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
var Jim = new Employee("jim", 22, "SoftWare Engineer");
var Sun = new Employee("Sun", 24, "Doctor");
Jim.sayName();
Sun.sayName();
在以上代码中Employee函数取代了CreateEmployee函数,Employee中的代码与CreateEmployee中的代码不同如下:
没有显示的创建对象
直接将属性和方法赋给了this对象
没有return语句
要创建Employee类的新实例,必须使用new操作符,实际会经历四个步骤:
创建一个新对象
将构造函数的作用域赋给新的对象
执行构造函数中代码
返回新对象
以上代码最后Jim和Sun中分别保存着Employee的两个不同实例,这两个实例都有一个constructor(构造函数)属性,该属性指向Employee,可以做
如下测试
alert(Jim instanceof Employee); //true
alert(Sun instanceof Employee);//true
同时这两个对象又都是Object类型的,可通过如下代码检测。
alert(Jim instanceof Object); //true
alert(Sun instanceof Object);//true
创建自定义的构造函数意味着将来可以把它的实例标石为一种特定的类型,而这也正是构造函数模式胜过工厂模式的地方。
下面详细讲解一下构造函数:
将构造函数当做函数
构造函数与其他函数的唯一区别就在于调用它们的方式不同。但是构造函数也是函数,不存在定义构造函数的特殊语法。其实,任何函数,只有通
过new来调用,那他就可以作为构造函数,例如,Employee除了以上用new方法调用外,还可以用如下方式来调用。
//作为普通函数调用
Employee("Sun", 28, "SoftWare Engineer"); //添加到window中
window.sayName();//Sun
//在另一个对象的作用域中调用
var o = new Object();
Employee.call(o, "Sun", 28, "SoftWare Engineer");
o.sayName();//Sum
这样就解决了两个函数做同一件事情的问题,但是新的问题又会出现了,这个在全局作用域中定义的函数,实际上只能被某个对象引用,而且最要
命的问题是,如果对象需要定义很多方法,那么就需要定义很多个全局函数,所以这个自定义的引用类型,就没有任何封装性可言了。
java按权重选取目标一个测试题
假设权重总和是total,那么在这个范围内产生一个随机数,观察这个随机数的所在区间,就能确定在哪个权重的范围之内了。
举个例子,有三个武将A、B、C,他们的出现机率分别是30%、40%和30%。首先产生一个随机数,这里的权重总和是100,分为三个区间,1~30,
31~70,71~100。自然随机数的范围也在100以内。假如这个随机数是49,很明显49是在31~70这个区间内,那么可确定该次随机产生的武将是B。思路是
这样,但是怎么用算法去实现呢?
对于确定的情况,一个最简单的方法是这样:
int rand = 49;//随机数,这里假设是个给定值 int A = 30; int B = 40; int C = 30; if(rand>0 && rand<=A){ return A; } else if(rand>A
&& rand<=A+B){ return B; } else if(rand>A+B && rand<=A+B+C){ return C; }
但是上面说过,这个权重的总和是不确定的,武将个数也不确定,这样做肯定不行。
后来想到了一个简单的办法:
int rand = 49;//随机数 int sum= 0; List<one> list = new ArrayList<one>();//假设这是一个武将列表 for(int i=0;i
Java 的 package 到底有何用处
让我们先了解一下,Java 的 package 到底有何用处。
其实,package 名称就像是我们的姓,而 class 名称就像是我们的名字。package 名称有很多 。 的,就好像是复姓。比如说 java.lang.String
,就是复姓 java.lang,名字为 String 的类别;java.io.InputStream 则是复姓
java.io,名字为 InputStream 的类别。
Java 会使用 package 这种机制的原因也非常明显,就像我们取姓名一样,光是一间学校的同一届同学中,就有可能会出现不少同名的同学,如果
不取姓的话,那学校在处理学生数据,或是同学彼此之间的称呼,就会发生很大的困扰。相同的,全世界的 Java 类别数量,恐怕比台湾人口还多,而
且还不断的在成长当中,如果类别不使用套件名称,那在用到相同名称的不同类别时,就会产生极大的困扰。幸运的是,Java 的套件名称我们可以自
己取,不像人的姓没有太大的选择 ( 所以有很多同名同姓的 ),如果依照 Sun 的规范来取套件名称,那理论上不同人所取的套件名称不会相同 (
请参阅 “命名惯例”的相关文章 ),也就不会发生名称冲突的情况。
可是问题来了,因为很多套件的名称非常的长,在写程序时,会多打好多字,花费不少时间,比如说:
java.io.InputStream is = java.lang.System.in;
java.io.InputStreamReader isr= new java.io.InputStreamReader(is);
java.io.BufferedReader br = new java.io.BufferedReader(isr);
实在是不美观又麻烦。于是,Sun 想了一个办法,就是 import.
这个 import 就是在程序一开头的时候,先说明程序中会用到那些类别的
简称,也就是只称呼名字,不称呼他的姓。首先,在档案开头写:
import java.lang.System;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
这几行说明了这四个姓名的类别,在程序中只用他的名字来称呼,所以当程序中提到 System 就是指 java.lang.System,而 InputStream 就是指
java.io.InputStream,依此类推。于是原来的程序就变成:
InputStream = System.in;
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
这样看起来是不是清爽多了呢?如果这些类别用的次数很多,那就更能体会到import 的好处了。可是这样还是不够,因为懒是人的天性,还是会
有人觉得打太多 import 了也很浪费时间,于是 Sun 又提供了一个方法:
import java.lang.*;
import java.io.*;
意思就是,等一下程序中提到的没有姓名的类别,不是姓 java.lang,就是姓java.io,如果这两个里面有同样名字的类别,而不幸的你又只用名
字称呼这个类别,那编译器仍然会跟你抱怨,因为它还是不知道你说的这个类别指那一
个姓的类别。那可不可以再懒一点呢,只写:
import java.*;
历史告诉我们,人可以懒,但不能太懒,这样是不行的。因为那些类别是姓 java.io 而不是姓 java.就像姓「诸葛」的人应该不会喜欢你称他为
「诸」先生吧。
为甚么我一开始说 import 跟 #include 不同呢?因为 import 的功能到此为止,它不像 #include 一样,会将档案内容载入进来。import 只是
请编译器帮你打字,让编译器把没有姓的类别加上姓,并不会把别的文件的程式码写进来。如果你想练习打字,可以不要使用 import,只要在用到类
别的时候,用它的全部姓名来称呼它就行了(就像例子一开始那样),跟使用 import 完全没有甚么两样。
另外,虽然人不可以太懒,但是 Sun 还是帮我们多偷了一点懒。因为java.lang 这个套件实在是太常太常太常用到了,几乎没有程序不用它的,
所以不管你有没有写 import java.lang;,编译器都会自动帮你补上,也就是说编译器只要看到没有姓的类别,它就会自动去 java.lang 里面找找看
,看这个类别是不是属于这个套件的。所以我们就不用特别去import java.lang了。
java中的String问题大剖析
1. String是一个对象
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
首先String不属于8种基本数据类型(byte, char, short, int, float, long, double, boolean)
2. new String()和new String(“”)都是申明一个新的空字符串,是空串不是null;
3. String str=”kvill”
String str=new String (“kvill”);的区别:
在这里,我们不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字
符串常量。
例1:
[java] view plaincopyprint?
private static void test01(){
String s0 = "kvill";
String s1 = "kvill";
String s2 = "kv" + "ill";
System.out.println(s0 == s1); // true
System.out.println(s0 == s2); // true
}
结果为:
true
true
首先,我们要知道Java会确保一个字符串常量只有一个拷贝。
因为例子中的s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字
符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”kvill”
的一个引用。
所以我们得出s0==s1==s2;
用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
例2:
[java] view plaincopyprint?
private static void test02(){
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = "kv" + new String("ill");
System.out.println(s0 == s1); // false
System.out.println(s0 == s2); // false
System.out.println(s1 == s2); // false
}
结果为:
false
false
false
例2中s0还是常量池中”kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分new String
(“ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。
4. String.intern()
再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个
String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一
个Unicode等于str的字符串并返回它的引用;看例3就清楚了
例3:
[java] view plaincopyprint?
private static void test03(){
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = new String("kvill");
System.out.println(s0 == s1); // false
s1.intern();
s2 = s2.intern();
System.out.println(s0 == s1); // false
System.out.println(s0 == s1.intern()); // true
System.out.println(s0 == s2); // true
}
结果为:
false
false //虽然执行了s1.intern(),但它的返回值没有赋给s1
true //说明s1.intern()返回的是常量池中”kvill”的引用
true
最后我再破除一个错误的理解:
有人说,“使用String.intern()方法则可以将一个String类保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方
法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中“如果我把他说的这个全局的String表理解为常量池的话
,他的最后一句话,“如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:
例4:
[java] view plaincopyprint?
private static void test04(){
String s1 = new String("kvill");
String s2 = s1.intern();
String s3 = "kvill";
System.out.println(s1 == s1.intern()); // false
System.out.println(s1 + " " + s2); // kvill kvill
System.out.println(s2 == s1.intern()); // true
System.out.println(s2 == s3); // true
}
结果:
false
kvill kvill
true
true
在这个类中我们没有声明一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill
”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。
s1==s1.intern()为false说明原来的“kvill”仍然存在;
s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。
5. 关于equals()和==
这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同,也就是是否是同一个字
符串的引用。
例5:
[java] view plaincopyprint?
private static void test05(){
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = s1;
System.out.println(s0.equals(s1)); // true
System.out.println(s0 == s1); // false
System.out.println(s2.equals(s1)); // true
System.out.println(s2 == s1); // true
}
结果:
true // 值相同
false // 地址不同(常量区和堆栈区)
true // 值相同
true // 同一个引用地址(都是堆栈区)
6. 关于String是不可变的
这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”;
就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,最后又和生成了
”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,
因为StringBuffer是可改变的
例6:
[java] view plaincopyprint?
private static void test06(){
String str = "kv" + "ill" + " " + "ans";
StringBuffer strBuf = new StringBuffer();
strBuf.append("kv").append("ill").append(" ").append("ans");
System.out.println(str + " : " + strBuf); // kvill ans : kvill ans
}
结果:
kvill ans : kvill ans