继承
Java三大特征之一:继承。Java的继承具有单继承的特点,每个子类只能有一个直接父类。
继承的特点
Java的继承用extends关键字来实现,被继承的类成为父类,实现继承的类被称为子类。子类和父类的关系就比如现实生活中儿子与父亲的关系。子类继承父类所有的“特点”,子类是父类的扩展,子类是一种特殊的父类,获得父类的全部属性和方法。
看下面的例子:
class Fruit { public double weight; public void info() { System.out.println("重量是:" + weight); } } public class Apple extends Fruit { public static void main(String[] args) { Apple a = new Apple(); a.weight = 23; a.info(); } } 结果就是:重量是:23.0
重写(覆盖)父类的方法
子类扩展了子类,在父类的基础上添加新的属性和功能。如果子类的某一功能(方法)跟父类的功能不同,则应该重写此方法。这种子类包含与父类同名方法的现象被称为方法重写,又称覆盖(Override)。子类覆盖父类的方法之后,子类的对象就不能访问父类的方法了,但是可以在子类中调用父类中被覆盖的方法。使用super关键字或者父类的类名作为调用者就可以调用父类中被覆盖的方法。对于属性同样,在子类的对象中也可以通过super来访问父类被覆盖的属性。
Java程序创建某个类的对象的时候,系统会隐式地创建该类父类的对象。只要有一个子类对象存在,则一定存在一个与之对应的父类对象。在子类方法中使用super引用时,super总是指向作为该方法调用者的子类对象所对应的父类对象。其实,super引用和this引用很像,其中this总是指向到调用该方法的对象,而super则指向this指向对象的父对象。
方法重载和方法重写的区别:
重载发生在同一个类中的多个同名方法之间,而重写发生在子类和父类的同名方法之间。二者之间没有太大的相似之处。当然,如果父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个父类方法有相同方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。
调用父类的构造函数
在子类中调用父类的构造函数的初始化代码,可以用super实现调用。
看下面的例子:
class Base { public double size; public String name; public Base(double size, String name) { this.size = size; this.name = name; } } public class Sub extends Base { public String color; public Sub(double size, String name, String color) { super(size, name); this.color = color; } public static void main(String[] args) { Sub s = new Sub(2.3, "hello", "red"); System.out.println(s.size + " -- " + s.name + " -- " + s.color); } } 结果为:2.3 -- hello -- red
当调用子类构造函数来初始化子类对象的时候,父类的构造函数总是在子类的构造函数之前执行,然后执行父类的构造时,又会追溯到其父类的构造函数…,因此,创建任何java对象时,最先执行的总是java.long.Object这个累的构造函数。
看下面的例子:
class Creature { public Creature() { System.out.println("Creaure无参构造函数"); } } class Animal extends Creature { public Animal(String name) { System.out.println("Animal带一个参数的构造函数,其name是:" + name); } public Animal(String name, int age) { //使用this来调用同一个重载的构造函数 this(name); System.out.println("Animal带两个参数的构造函数,其age是:" + age); } } public class Dog extends Animal { public Dog() { super("小狗", 10); System.out.println("Dog无参构造函数"); } public static void main(String[] args) { new Dog(); } }
结果为:
封装
概念理解
封装是指将对象的状态信息隐藏在对象内部,不允许外部的程序直接访问对象内部的信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
封装是面向对象编程语言对客观世界的模拟,客观世界里的属性都是被隐藏在对象内部,外界无法直接操作和修改。
封装的好处很多:
1、 隐藏类的实现细节
2、 让使用者只能通过预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。
3、 可进行数据检查,从而有利于保证对象信息的完整性。
4、 便于修改,提高代码的可维护性。
包的概念
遇到类重名的情况怎么办?这时候就需要包来解决这个问题。
Java提供包(package)机制,用来提供多层命名空间,来解决类的命名冲突、类文件管理等问题。从逻辑概念来看,包是类的集合,一个包中可以包括多个类;从存储概念来看,包是类的组织方式,一个包对应一个文件夹,一个文件夹中包含多个java源文件;包和类的关系就像文件夹与文件的关系。
Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元,如果希望把一个类放在指定的包结构下,我们应该在java源程序的第一个非注释行写如下格式的代码:
package packageName; |
一旦在java源文件中使用了这个package语句,则意味着该源文件里定义的所有类都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合。
看下面的例子:
package helloPack;
public class Hello
{
public static void main(String[] args)
{
System.out.println("ni hao!");
}
}
在命令行下输入:,-d用于设置编译生产class文件的保存位置,.表示当前路径。使用这一句命令时候,在当前路径下出现一个helloPack的文件夹,此文件夹中有一个Hello.class的文件。此时可以运行,可以运行程序。但是如果进入helloPack文件来运行Hello的话,就会出现错误:
import的使用
如果一个类调用不再同一个包下的类的对象的时候,就必须使用全名,也就是加上包名。如果包的结构很复杂的话,类的全名就会很长,为了简化编程,java提供了import关键字,它可以想某一个源文件中导入指定包层次下某个类或全部类,import语句应该放在package之后,类的定义之前。
比如:
import hello.subhello; import java.util.*; import java.sql.Date;
*的含义就是此包下面的所有的类。一般可以直接用*代替某一个需要的类,但是有时候必须明确指定具体的某一个类,也就是必须写出其完整的包路径,就比如import java.sql.Date;。
访问控制符
Java提供了三种访问控制符:private、protected和public,分别代表三个访问控制级别。当然系统还有一个默认的访问控制符:default,当不实用任何访问控制符来修饰类或类成员的时候,系统默认使用此访问控制级别。
private:如果类里的一个属性或方法使用了private,则这个成员只能在该类内部来访问。这个访问控制符来修饰属性最合适,可以把属性隐藏在类的内部。
default:如果类的一个属性或方法不实用任何访问控制符,系统就会给它一个默认的访问控制符。
protected:如果类的一个属性或方法使用protected访问控制符,那么这个成员既可以被同一个包中其他类访问,也可以被不同包中的子类访问。如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法。
public:如果类的一个属性或方法使用public来修饰,那么它就可以被所有的类访问,不管访问类和被访问类是否处于同一个包中,不管是否有继承关系。
访问控制表:
private |
default |
protected |
public |
|
同一个类中 |
√ |
√ |
√ |
√ |
同一个包中 |
|
√ |
√ |
√ |
子类中 |
|
|
√ |
√ |
全局范围内 |
|
|
|
√ |
可以发现,访问控制符是用来控制一个类的成员是否可以被其他类访问,对于局部变量而言,其作用域就是它所在的方法,不可能被其他类来访问,因此不能使用控制符来修饰。
看下面的例子:
public class Person { // private String name; private int age; // public void setName(String name) { // if(name.length() > 6 || name.length() < 2) { System.out.println("您的设置不符合要求"); return; } else { this.name = name; } } public String getName() { return this.name; } // public void setAge(int age) { if(age > 100 || age < 0) { System.out.println("您设置的年龄不合法"); return; } else { this.age = age; } } public int getAge() { return this.age; } }
public class TestPerson { public static void main(String[] args) { Person p = new Person(); p.setAge(120); System.out.println("设置年龄不成功时:" + p.getAge()); p.setAge(22); System.out.println("设置年龄成功时:" + p.getAge()); p.setName("JACK"); System.out.println("设置姓名成功时:" + p.getName()); } }
运行结果是:
Person类中的name和age属性被private修饰,因而只能在person类中才可以操作和访问,在Person类之外只能通过每个属性对应的setter和getter方法来操作和访问它们。TestPerson类中不能直修改,只能通过setter来修改。使用setter方法来操作属性的好处就是程序员可以在setter方法中添加自己的逻辑控制,以防止出现与实际需要不符的情况。
程序设计的时候,应该尽量避免一个模块直接操作和访问另一个模块的数据,模块设计追求高内聚(尽可能把模块的内部数据、功能实现细节隐藏在模块内部独立完成,不允许外部直接干预)、低耦合(仅暴露少量的方法给外部使用)。如果一个java类的每个属性都被使用private修饰,并为每个属性都提供了public修饰setter和getter方法,这个类就是一个符合JavaBean规范的类。因此,JavaBean总是一个封装良好的类。
使用原则
1) 类的绝大部分属性都应该使用private修饰,除了一些static修饰的、类似全局变量的属性,才可能考虑使用public修饰。初次之外,有些方法只是用于辅助实现该类的其他方法,这些方法被成为工具方法,工具方法也应该使用private修饰。
2) 如果某个类作为一个父类,该类的大部分方法可能仅希望被其子类重写,则应该使用protected修饰。
3) 希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,暴露给其他类中创建该类的对象。因为顶级类通常都希望被其他类自由使用,所以大部分顶级类都是用public修饰。
多态
多态性是面向对象的核心特征之一。在面向对象语言中,多态性是指一个方法可以有多个实现的版本。对于一个方法的多种实现,程序运行时,系统会根据方法的参数或调用方法的对象自动选择一个合适的方法执行,不会产生混乱。
类的多态性表现为方法的多态性,讨论在不同层次的类中以及在同一个类中,多个同名方法之间的关系问题。方法的多态性主要表现在方法的重载和方法的覆盖。
方法的重载
重载是指同一个类中的多个方法可以同名但是参数列表不同。
方法重载的具体内容我已经在上一篇博客中介绍过,详见:点击打开。
方法的覆盖
覆盖是指子类重新定义了父类的同名方法。详见此篇博客上部分。
运行时多态
多态性有两种:编译时多态和运行时多态。
1. 编译时多态性
对于多个同名方法,如果在编译时就能确定执行同名方法的哪一个,则称为编译时多态。方法的重载就是编译时多态。
2. 运行时多态性
如果在编译时不能确定多个同名方法的哪一个,只能在运行的时候才能确定,就称为是运行时多态。
方法的覆盖表现两种多态性,当对象获得本类的实例时,是编译时多态,否则就是运行时多态。
例如:
假如有一个父类Person和一个子类Student,各自都有一个print方法。
Person p = new Person(); Student s = new Student(); p.print();//编译时多态性,执行Person中的方法 s.print();//编译时多态性,执行Student中的方法
由于子类对象即是父类对象,父类对象与子类对象之间具有赋值相容性,父类对象能够被赋值为子类对象。
Person p2 = new Student(); p2.print();//运行时多态,执行Student类的方法。
此时,p2声明为父类对象却获得了Student的对象,这样有两种情况,如果子类覆盖了父类的方法,则执行子类的方法;否则执行父类的方法。在编译的时候,仅仅依据对象所属的类,系统无法确定到底该执行哪个类的方法,只有到运行的时候才能确定,故称为运行时多态。
再看下面的例子:
class BaseClass { public int book = 6; public void base() { System.out.println("父类的普通方法"); } public void test() { System.out.println("父类被覆盖的方法"); } } public class SubClass extends BaseClass { public String book= "liuyifei"; public void test() { System.out.println("子类覆盖父类的方法"); } public void sub() { System.out.println("子类的普通方法"); } public static void main(String[] args) { BaseClass bc = new BaseClass(); System.out.println(bc.book); bc.base(); bc.test(); SubClass sc = new SubClass(); System.out.println(sc.book); sc.base(); sc.test(); BaseClass b = new SubClass(); System.out.println(b.book); b.base(); b.test(); } }
运行结果是:
注:如有错误,请指出。