继承让我们更加容易实现类的扩展。比如,我们定义了人类,在定义Boy类就只需要扩展人类即可。实现了代码的重用,不用再重新发明轮子。
从英文字面意思理解,extends 的意思是“扩展”。子类是父类的扩展。现实世界中的继承无处不在。
上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果重新定义一个Student类,发现已经有Person类包含了我们需要的属性和方法,那么Student类只需要继承Person类即可拥有Person类的属性和方法。
【示例5-1】使用extends实现继承
public class Test{
public static void main(String[] args) {
Student s = new Student("高淇",172,"Java");
s.rest();
s.study();
}
}
class Person {
String name;
int height;
public void rest(){
System.out.println("休息一会!");
}
}
class Student extends Person {
String major; //专业
public void study(){
System.out.println("在尚学堂,学习Java");
}
public Student(String name,int height,String major) {
//天然拥有父类的属性
this.name = name;
this.height = height;
this.major = major;
}
}
instanceof 是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:
public class Test{
public static void main(String[] args) {
Student s = new Student("高淇",172,"Java");
System.out.println(s instanceof Person);
System.out.println(s instanceof Student);
}
}
两条语句的输出结果都是true。
子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件。
方法的重写需要符合下面的三个要点:
【示例5-3】方法重写
public class TestOverride {
public static void main(String[] args) {
Vehicle v1 = new Vehicle();
Vehicle v2 = new Horse();
Vehicle v3 = new Plane();
v1.run();
v2.run();
v3.run();
v2.stop();
v3.stop();
}
}
class Vehicle { // 交通工具类
public void run() {
System.out.println("跑....");
}
public void stop() {
System.out.println("停止不动");
}
}
class Horse extends Vehicle { // 马也是交通工具
public void run() { // 重写父类方法
System.out.println("四蹄翻飞,嘚嘚嘚...");
}
}
class Plane extends Vehicle {
public void run() { // 重写父类方法
System.out.println("天上飞!");
}
public void stop() {
System.out.println("空中不能停,坠毁了!");
}
}
执行结果如图所示:
Object 类是所有 Java 类的根基类,也就意味着所有的Java 对象都拥有 Object 类的属性和方法。如果在类的声明中未使用 extends 关键字指明其父类,则默认继承Object类。
【示例5-4】Object类
public class Person {
...
}
//等价于:
public class Person extends Object {
...
}
Object 类中定义有 public String toString() 方法,其返回值是 String 类型。Object 类中 toString 方法的源码为:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
根据如上源码得知,默认会返回 “类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。
【示例5-5】 toString() 方法测试和重写toString()方法
class Person {
String name;
int age;
@Override
public String toString() {
return name+",年龄:"+age;
}
}
public class Test {
public static void main(String[] args) {
Person p=new Person();
p.age=20;
p.name="李东";
System.out.println("info:"+p);
Test t = new Test();
System.out.println(t);
}
}
“==” 代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象
Object 类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id 相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
Object 的 equals 方法默认就是比较两个对象的 hashcode ,是同一个对象的引用时返回 true 否则 返回flase。但是,我们可以根据我们自己的要求重写 equals 方法。
【示例5-6】equals 方法测试和自定义类重写 equals 方法
public class TestEquals {
public static void main(String[] args) {
Person p1 = new Person(123,"高淇");
Person p2 = new Person(123,"高小七");
System.out.println(p1==p2); //false,不是同一个对象
System.out.println(p1.equals(p2)); //true,id相同则认为两个对象内容相同
String s1 = new String("尚学堂");
String s2 = new String("尚学堂");
System.out.println(s1==s2); //false, 两个字符串不是同一个对象
System.out.println(s1.equals(s2)); //true, 两个字符串内容相同
}
}
class Person {
int id;
String name;
public Person(int id,String name) {
this.id=id;
this.name=name;
}
public boolean equals(Object obj) {
if(obj == null){
return false;
}else {
if(obj instanceof Person) {
Person c = (Person)obj;
if(c.id==this.id) {
return true;
}
}
}
return false;
}
}
JDK 提供的一些类,如 String、Date、包装类等,重写了Object 的 equals 方法,调用这些类的 equals 方法。x.equals(y),当x 和 y 所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则 返回 false。
super 是直接父类对象的引用。可以通过super 来访问父类中被子类覆盖的方法或属性。
使用 super 调用普通方法,语句没有位置限制,可以子类中随便调用。
若是构造方法的第一行代码没有显式地调用 super(…)或者 this(…);那么Java默认都会调用 super(),含义是调用父类的无参数构造方法。这里的super()可以省略。
【示例5-7】super 关键字的使用
public class TestSuper01 {
public static void main(String[] args) {
new ChildClass().f();
}
}
class FatherClass {
public int value;
public void f(){
value = 100;
System.out.println ("FatherClass.value="+value);
}
}
class ChildClass extends FatherClass {
public int value;
public void f() {
super.f(); //调用父类对象的普通方法
value = 200;
System.out.println("ChildClass.value="+value);
System.out.println(value);
System.out.println(super.value); //调用父类对象的成员变量
}
}
属性/方法查找顺序:(比如:查找变量h)
构造方法嗲用顺序
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到 Object,然后再依次向下执行类的初始化调用顺序,与构造方法调用顺序一样,不在重复。
【示例 5-8】构造方法向上追溯执行测试
public class TestSuper02 {
public static void main(String[] args) {
System.out.println("开始创建一个ChildClass对象......");
new ChildClass();
}
}
class FatherClass {
public FatherClass() {
System.out.println("创建FatherClass");
}
}
class ChildClass extends FatherClass {
public ChildClass() {
System.out.println("创建ChildClass");
}
}
我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露点单的接口,比如:电源开关。具体内容是怎么实现的,我们不需要操心。
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
我们程序设计要追求 “高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
编程中封装的具体优点:
【示例5-9】没有封装的代码会出现一些问题
class Person {
String name;
int age;
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.name = "小红";
p.age = -45;//年龄可以通过这种方式随意赋值,没有任何限制
System.out.println(p);
}
}
我们都知道,年龄不可能是负数,也不可能超过 130 岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图所示
再比如说,如果我们哪天我们需要Person 类中的age 属性改为String 类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下 Person 类的 setAge()方法即可,而无需修改使用了该类的客户代码。
Java 是使用 “访问控制符” 来控制哪些细节需要封装,哪些细节需要暴露的。Java 中 4种 “访问控制符” 分别为 private、default、protected、public ,他们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。
下面做进一步说明 Java 中 4种访问权限修饰符的区别:首先我们创建4 个类:Person 类、Student类、Animal 类和 Computer 类、分别比较本类、本包、子类、其他包的区别:
public 访问权限修饰符:
总结:public 修饰符的访问权限为:该项目的所有包中的所有类。
protected 访问权限修饰符:将Person 类中属性改为 protected,其他类不修改
总结:protected 修饰符的访问权限为:同一个包中的类以及其他包中的子类。
默认访问权限修饰符:将Person 类中属性改为默认的,其他类不修改
private 访问权限修饰符:将Person类中属性改为 private ,其他类不修改。
总结:private 修饰符的访问权限为:同一个类。
【示例5-10】JavaBean 的 封装实例
public class Person {
// 属性一般使用private修饰
private String name;
private int age;
private boolean flag;
// 为属性提供public修饰的set/get方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
【示例5-11】 封装的使用
class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
// this.age = age;//构造方法中不能直接赋值,应该调用setAge方法
setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
//在赋值之前先判断年龄是否合法
if (age > 130 || age < 0) {
this.age = 18;//不合法赋默认值18
} else {
this.age = age;//合法才能赋值给属性age
}
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test2 {
public static void main(String[] args) {
Person p1 = new Person();
//p1.name = "小红"; //编译错误
//p1.age = -45; //编译错误
p1.setName("小红");
p1.setAge(-45);
System.out.println(p1);
Person p2 = new Person("小白", 300);
System.out.println(p2);
}
}
执行结果如图所示:
多态指定的是一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。比如:同样是调用人的“休息”方法,张三是睡觉的,李四是旅游;同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
【示例 5-12】 多态和类型转换测试
class Animal {
public void shout() {
System.out.println("叫了一声!");
}
}
class Dog extends Animal {
public void shout() {
System.out.println("旺旺旺!");
}
public void seeDoor() {
System.out.println("看门中....");
}
}
class Cat extends Animal {
public void shout() {
System.out.println("喵喵喵喵!");
}
}
public class TestPolym {
public static void main(String[] args) {
Animal a1 = new Cat(); // 向上可以自动转型
//传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
animalCry(a1);
Animal a2 = new Dog();
animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
//编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
// 否则通不过编译器的检查。
Dog dog = (Dog)a2;//向下需要强制类型转换
dog.seeDoor();
}
// 有了多态,只需要让增加的这个类继承Animal类就可以了。
static void animalCry(Animal a) {
a.shout();
}
/* 如果没有多态,我们这里需要写很多重载的方法。
* 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
static void animalCry(Dog d) {
d.shout();
}
static void animalCry(Cat c) {
c.shout();
}*/
}
执行结果:
示例 5-12 给大家展示了多态最为常见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的方式。
由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我们不能使用父类的引用变量调用 Dog 类特有的 seeDoor()方法。
那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章所讲的内容:对象的转型。
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!
【示例5-13 对象的转型
public class TestCasting {
public static void main(String[] args) {
Object obj = new String("北京尚学堂"); // 向上可以自动转型
// obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
/* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
* 不然通不过编译器的检查。 */
String str = (String) obj; // 向下转型
System.out.println(str.charAt(0)); // 位于0索引位置的字符
System.out.println(obj == str); // true.他们俩运行时是同一个对象
}
}
在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常 ClassCastException。
【示例 5-14】 类型转换异常
public class TestCasting2 {
public static void main(String[] args) {
Object obj = new String("北京尚学堂");
//真实的子类类型是String,但是此处向下转型为StringBuffer
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
执行结果为:
为了避免出现这种异常,我们可以使用 5.1.2 中所学的 instanceof 运算符进行判断:
public class TestCasting3 {
public static void main(String[] args) {
Object obj = new String("北京尚学堂");
if(obj instanceof String){
String str = (String)obj;
System.out.println(str.charAt(0));
}else if(obj instanceof StringBuffer){
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
}
执行结果:
final 关键字的作用
final int MAX_SPEED = 120;
final void study(){}
3。 修饰类:修饰的累不鞥被继承。比如:Math、String等。
final class A {}
final 修饰变量详见 第二章示例 2-9
final 修饰方法:
final 修饰类:
使用abstract 修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
包含抽象方法的类就是抽象类。通过abstract 方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。
【示例5-16】抽象类和抽象方法的基本用法
//抽象类
abstract class Animal {
abstract public void shout(); //抽象方法
}
class Dog extends Animal {
//子类必须实现父类的抽象方法,否则编译错误
public void shout() {
System.out.println("汪汪汪!");
}
public void seeDoor(){
System.out.println("看门中....");
}
}
//测试抽象类
public class TestAbstractClass {
public static void main(String[] args) {
Dog a = new Dog();
a.shout();
a.seeDoor();
}
}
接口就是比“抽象类”还“抽象”的抽象类,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。
抽象类好提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了以批类具有的公共方法规范。
从接口的实现者角度看,接口定义了可以向外部提供的服务。
从接口的调用者角度看,接口定义了实现者能提供哪些服务。
接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。
接口和实现类不是父子关系,是实现规则的关系。比如:我定义了一个接口 Runnable,Car 实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现 Runnable 接口。
接口就是规范,定义的是一组规则,体现了实现世界中“如果你是。。则必须能。。”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。
接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。
面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。
区别
声明格式:
[访问修饰符] interface 接口名 [extends 父接口1,父接口2…] {
常量定义;
方法定义;
}
定义接口的详细说明:
要点
【示例5-17】接口的作用
public class TestInterface {
public static void main(String[] args) {
Volant volant = new Angel();
volant.fly();
System.out.println(Volant.FLY_HIGHT);
Honest honest = new GoodMan();
honest.helpOther();
}
}
/**飞行接口*/
interface Volant {
int FLY_HIGHT = 100; // 总是:public static final类型的;
void fly(); //总是:public abstract void fly();
}
/**善良接口*/
interface Honest {
void helpOther();
}
/**Angle类实现飞行接口和善良接口*/
class Angel implements Volant, Honest{
public void fly() {
System.out.println("我是天使,飞起来啦!");
}
public void helpOther() {
System.out.println("扶老奶奶过马路!");
}
}
class GoodMan implements Honest {
public void helpOther() {
System.out.println("扶老奶奶过马路!");
}
}
class BirdMan implements Volant {
public void fly() {
System.out.println("我是鸟人,正在飞!");
}
}
执行结果:
接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。
【示例 5-18】 接口的对继承
interface A {
void testa();
}
interface B {
void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
void testc();
}
public class Test implements C {
public void testc() {
}
public void testa() {
}
public void testb() {
}
}
面向接口编程时面向对象编程的一部分。
为什么需要面向接口编程?软件设计中最难处理的就是需求的复杂变化,需求的变化更多的体现在具体实现上。我们的编程如果围绕具体实现来展开就会陷入“复杂变化”的汪洋大海中,软件也就不能最终实现。我们必须围绕某种稳定的东西开展,才能以静制动,实现规范的高质量的项目。
接口就是规范,就是项目中最稳定的东东!面向接口编程可以让我们把握住真正核心的东西,使实现复杂多变的需求成为可能。
通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,日高整个系统的可扩展性和可维护性。
面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!
老鸟建议
接口语法本身非常简单,但是如何真正使用?这才是大学问。我们需要在后面的项目中反复使用,大家才能体会到。学到此处,能了解基本概念,熟悉基本语法,就是“好学生”了,请继续努力!再请工作后,闲余时间在看看上面这段话,相信你会有更深的体会。
一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。
内部类可以使用 public、default、protected、private 以及 static 修饰。而外部顶级类(我们以前接触的类)只能使用 public 和 default 修饰。
注意
内部类只是一个编译时概念,一旦我们编译成功,就会称为完全不同的两个类。对于一个名为 Outer 的外部类和其内部定义的名为 Inner 的内部类。编译完成后会出现 Outer.class he Outer$Inner.class 两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类相同。
【示例5-19】 内部类介绍
/**外部类Outer*/
class Outer {
private int age = 10;
public void show(){
System.out.println(age);//10
}
/**内部类Inner*/
public class Inner {
//内部类中可以声明与外部类同名的属性与方法
private int age = 20;
public void show(){
System.out.println(age);//20
}
}
}
示例5-19 编译之后会产生两个不同的字节码文件:
在Java 中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。
成员内部类(可以使用 private、default、protected、public 任意进行修饰。类文件:外部类$内部类.class)
非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部对象。非静态内部类对象单独属于外部类的某个对象。
非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。
非静态内部类不能有静态方法、静态属性和静态初始化块。
外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。
成员变量访问要点:
【示例5-20】 成员变量的访问要点
class Outer {
private int age = 10;
class Inner {
int age = 20;
public void show() {
int age = 30;
System.out.println("内部类方法里的局部变量age:" + age);// 30
System.out.println("内部类的成员变量age:" + this.age);// 20
System.out.println("外部类的成员变量age:" + Outer.this.age);// 10
}
}
}
内部类的访问:
new Inner()
Outer.Inner varname = new Outer().new Inner()
【示例5-21】 内部类的访问
public class TestInnerClass {
public static void main(String[] args) {
//先创建外部类实例,然后使用该外部类实例创建内部类实例
Outer.Inner inner = new Outer().new Inner();
inner.show();
Outer outer = new Outer();
Outer.Inner inn = outer.new Inner();
inn.show();
}
}
执行结果:
定义方式
static class ClassName {
//类体
}
使用要点
【示例5-22】静态内部类的访问
class Outer{
//相当于外部类的一个静态成员
static class Inner{
}
}
public class TestStaticInnerClass {
public static void main(String[] args) {
//通过 new 外部类名.内部类名() 来创建内部类对象
Outer.Inner inner =new Outer.Inner();
}
}
适合哪种只需要使用一次的类。比如:键盘监听操作等等。
语法:
new 父类构造器(实参类表) \实现接口 () {
//匿名内部类类体!
}
【示例5-23】 匿名内部类的使用
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
}
);
this.addKeyListener(new KeyAdapter(){
@Override
public void keyPressed(KeyEvent e) {
myTank.keyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
myTank.keyReleased(e);
}
}
);
注意
还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。
局部内部类的使用主要是用来解决比较复杂的问题,想创建一个类来复制我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。
【示例5-24】 方法中的内部类
public class Test2 {
public void show() {
//作用域仅限于该方法
class Inner {
public void fun() {
System.out.println("helloworld");
}
}
new Inner().fun();
}
public static void main(String[] args) {
new Test2().show();
}
}
【示例5-25】 String 类的实例
String e = "" ; // 空字符串
String greeting = " Hello World ";
【示例5-26】 字符串连接
String s1 = "Hello";
String s2 = "World! ";
String s = s1 + s2; //HelloWorld!
6、 符号 + 把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。
7. 当 + 运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串后再进行连接。
【示例5-27】 + 连接符
int age = 18;
String str = "age is" + age; //str赋值为"age is 18"
//这种特性通常被用在输出语句中:
System.out.println("age is" + age);
在Java 的内存分析中,我们会经常听到关于 “常量池” 的描述,实际上常量池也分了以下三种:
全局字符串常量池中存放的内容是在类加载完成后存到 String Pool 中的,在每个 VM 中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)
class 常量池是在编译的时候每个 class 都有的,在编译阶段,存放的是 常量(文本字符串、final 常量等)和符号引用。
运行时常量池是在类加载之后,将每个class 变量池中的符号引用值转存到运行时常量池中,也就是说,每个class 都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
【示例 5-28】常量池
String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);// true
System.out.println(str2 == str4);// false
System.out.println(str4 == str5);// true
首先经过编译之后,在该类的class 变量池中存放一些顾浩引用,然后类加载之后,将class 常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的“abc”实例对象),然后将这个对象的引用存到全局 String Pool 中,也就是 String Pool 总,最后在解析阶段,要把运行时常量池中的符号引用地换成直接引用,那马就直接查询 String Pool ,保证 String Pool 里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。
回到示例5-28 的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个 “abc” 实例,全局 String Pool 中存放着 “abc” 的一个引用值,然后在运行第二句的时候会生成两个实例,一个是 “def” 的实例对象,并且 String Pool 中存储一个“def”的引用值,还有一个是 new 出来的 一个 “def” 的实例对象,与上面那个是不同的实例,当在解析 str3 的时候查找 String Pool ,里面有“abc”的全局驻留字符串引用,所以str3 的引用地址与之前的那个已存在的相同,str4是在运行的时候调用 intern()方法,返回 String Pool 中 “def” 的引用值,如果没有就将 str2 的引用值添加进去,在这里,String Pool 中已经有了 “def” 的引用值,最后 str5 在解析的时候就也是指向存在于 String Pool 中的 “def” 的引用值,那么这样一分析之后,结果就容易理解了。
http://www.oracle.com/technetwork/java/javase/documentation/jdk8-doc-downloads-2133158.html
注意
eclipse 中将鼠标放到类或方法上, 即可看到相关的注释说明;再按下 F2 即可将注释窗口固定。
String类是我们最常使用的类。字符串类的方法我们必须非常熟悉!我们列出常用的方法,请大家熟悉。
public class StringTest1 {
public static void main(String[] args) {
String s1 = "core Java";
String s2 = "Core Java";
System.out.println(s1.charAt(3));//提取下标为3的字符
System.out.println(s2.length());//字符串的长度
System.out.println(s1.equals(s2));//比较两个字符串是否相等
System.out.println(s1.equalsIgnoreCase(s2));//比较两个字符串(忽略大小写)
System.out.println(s1.indexOf("Java"));//字符串s1中是否包含Java
System.out.println(s1.indexOf("apple"));//字符串s1中是否包含apple
String s = s1.replace(' ', '&');//将s1中的空格替换成&
System.out.println("result is :" + s);
}
}
【示例5-30】 String类常用方法二
public class StringTest2 {
public static void main(String[] args) {
String s = "";
String s1 = "How are you?";
System.out.println(s1.startsWith("How"));//是否以How开头
System.out.println(s1.endsWith("you"));//是否以you结尾
s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止
System.out.println(s);
s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7
System.out.println(s);
s = s1.toLowerCase();//转小写
System.out.println(s);
s = s1.toUpperCase();//转大写
System.out.println(s);
String s2 = " How old are you!! ";
s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除
System.out.println(s);
System.out.println(s2);//因为String是不可变字符串,所以s2不变
}
}
【示例5-31】 忽略大小写的字符串比较
"Hello".equalsIgnoreCase("hellO");//true
【示例5-32】字符串的比较 “==” 与 equals()方法
public class TestStringEquals {
public static void main(String[] args) {
String g1 = "北京尚学堂";
String g2 = "北京尚学堂";
String g3 = new String("北京尚学堂");
System.out.println(g1 == g2); // true 指向同样的字符串常量对象
System.out.println(g1 == g3); // false g3是新创建的对象
System.out.println(g1.equals(g3)); // true g1和g3里面的字符串内容是一样的
}
}
示例5-32的内存分析:
开闭原则(Open-Closed Principe)就是让设计的系统对罗占开放,对修改封闭
对扩展开放:
就是指,应对需求变化要灵活。要增加新功能时,不需要修改已有的代码,增加代码即可。
对修改关闭:
就是指,核心部分经过精心设计后,不在因为需求变化而改变。
在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。
模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。详见抽象类部分示例。
其实在Java 开发中,还有另外一个方法可以实现同样的功能,那就是 Java 回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类汇总调用B类总的C方法,然后 B类中的C方法 中反过来调用 A类中的 D方法,那么D这个方法就叫回调方法。
回调的具体过程如下:
这样说大家可能好事不太理解,下面我们根据示例5-33来说明回调机制。
【示例5-33】 回调机制示例
/**
* 回调接口
*/
interface CallBack {
/**
* 小高知道答案后告诉小刘时需要调用的方法,即回调方法
* @param result 是问题的答案
*/
public void answer(String result);
}
/*
/**
* 小刘类:实现了回调接口CallBack(背景一)
*/
class Liu implements CallBack {
/**
* 包含小高对象的引用 (背景二)
*/
private Gao gao;
public Liu(Gao gao){
this.gao = gao;
}
/**
* 小刘通过这个方法去问小高
* @param question 小刘问的问题“学习Java选哪家机构呢?”
*/
public void askQuestion(String question){
//小刘问小高问题
gao.execute(Liu.this, question);
}
/**
* 小高知道答案后调用此方法告诉小刘
*/
@Override
public void answer(String result) {
System.out.println("小高告诉小刘的答案是:" + result);
}
}
/**
* 小高类
*/
class Gao {
/**
* 相当于class B有一个参数为CallBack的方法C(背景三)
*/
public void execute(CallBack callBack, String question){
System.out.println("小刘问的问题是:" + question);
//模拟小高挂点后先办自己的事情花了很长时间
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//小高办完自己的事情后想到了答案
String result = "学Java当然去北京尚学堂";
//小高打电话把答案告诉小刘,相当于class B 反过来调用class A 的D方法
callBack.answer(result);
}
}
public class Test {
public static void main(String[] args) {
Gao gao= new Gao();
Liu liu = new Liu(gao);
//小刘问问题
liu.askQuestion("学习Java选哪家机构呢?");
}
}
组合模式是将对象组合成属性结构以表示“部分-整体”的层次结构。组合模式使用得用户对单对象和组合对象的使用具有一致性。
【示例5-34】对象的组合
class Cpu {
public void run() {
System.out.println("quickly.........");
}
}
class MainBoard {
public void connect() {
System.out.println("connect...........");
}
}
class Memory {
public void store() {
System.out.println("store........");
}
}
public class Computer {
Cpu cpu;
Memory memory;
MainBoard mainBoard;
public void work() {
cpu.run();
memory.store();
mainBoard.connect();
}
public static void main(String[] args) {
Computer computer = new Computer();
computer.cpu = new Cpu();
computer.mainBoard = new MainBoard();
computer.memory = new Memory();
computer.work();
}
}
Java 使用默认的值对其初始化。
– 构造器又叫构造方法(constructor),用于构造该类的实例。Java 通过 new 关键字来调用构造方法,从而返回该类的实例,是一种特殊的方法。
方法的重载是指一个类中可以定义有相同的名字,但参数不同的多个方法。调用时,会根据不同的参数表选择对应的方法。
this 关键字的作用