四、三大特性
封装
说到封装,基本上是个程序员都用过。我们在写一个方法的时候,不会将一个方法体写的特别长,
而是选择将方法拆解成好几个方法,将不同逻辑的方法进行封装。Java 的封装比一般的方法封装更加系统化一些。
概念:将类的成员变量隐藏在类的内部不允许外部程序直接访问,仅可通过该类提供的方法来实现对隐藏信息的操作和访问。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让代码更容易理解和维护,也加强了代码的安全性。
优点:
- 良好的封装能够减少耦合;
- 类内部的结构可以自由修改;
- 可以对成员变量进行更精确的控制;
- 隐藏信息,实现细节
措施:
1、访问修饰符
访问修饰符 | 本类 | 本包 | 子类 | 其他 |
---|---|---|---|---|
private | Y | N | N | N |
默认 | Y | Y | N | N |
protected | Y | Y | Y | N |
public | Y | Y | Y | Y |
从 private 到 public ,封装性越来越差
2、this 关键字
使用 this 关键字是为了区别类提供的公共方法中对于函数参数和成员参数名的区别,如
public void setName(String name){
this.name = name;
}
3、Java 中的内部类
Java 内部类提供了更好的封装,可以将内部类隐藏在外部类之内,不允许同一个包中的其他类直接访问内部类。
根据内部类的实现方式,我们将内部类分为:成员内部类、静态内部类、方法内部类以及匿名内部类。内部类和接口的配合,很好地实现了多继承的效果。
成员内部类
成员内部类是最常见的内部类,代码如下:
public class OutterClass {
public String outterName = "OutterClass";
private String outterSecret = "outterSecret";
public static String method = "test";
public Integer age = 2;
public void test() {
// 外部类无法直接调用内部类的属性和方法
// System.out.println(innerName);
}
public class InnerClass{
public String innerName = "InnerClass";
private String innerSecret = "innersecret";
public Integer age = 1;
public void test() {
// 可以访问外部类的成员属性,public、private 修饰的都可以
System.out.println(outterName);
System.out.println(outterSecret);
// 内部类和外部类存在相同名称属性的情况
System.out.println(age);
System.out.println(this.age);
System.out.println(OutterClass.this.age);
}
}
public static void main(String[] args) {
OutterClass outterClass = new OutterClass();
// 通过实例化内部类的方式调用内部类方法,成员内部类没有 static 属性和方法
InnerClass innerClass = outterClass.new InnerClass();
innerClass.test();
}
}
从上面代码可以看到成员内部类的使用方法和注意事项:
- 成员内部类如同外部类的成员一样定义
- 成员内部类可以调用外部类的属性和方法,即便是 private 修饰的变量
- 外部类无法直接调用成员内部类的属性和方法,只能通过实例化成员内部类的方式调用,实例化的方式为
内部类 对象名 = 外部类实例.new 内部类()
- 成员内部类和外部类有相同属性的情况下,内部类方法默认调用内部类属性,使用
外部类名.this.属性
的方式调用外部类属性 - 在编译外部类时,我们会发现产生了两个.class文件
静态内部类
静态内部类是 static 修饰的内部类,代码如下:
public class OutterClass {
public String outterName = "OutterClass";
private String outterSecret = "outterSecret";
public static String method = "test";
public Integer age = 2;
public void test() {
// 外部类无法直接调用内部类的属性和方法
// System.out.println(innerName);
}
public static class InnerClass{
public String innerName = "InnerClass";
private String innerSecret = "innersecret";
public static String method = "test2";
public Integer age = 1;
public void test() {
// 无法直接访问外部类非静态的成员的方法,但可以通过实例化外部类的方式访问
// System.out.println(outterName);
// System.out.println(outterSecret);
System.out.println((new OutterClass()).outterName);
// 内部类和外部类存在相同名称属性的情况
System.out.println(method);
System.out.println(this.method);
System.out.println(OutterClass.method);
}
}
public static void main(String[] args) {
// OutterClass outterClass = new OutterClass();
// 访问静态内部类时,不需要实例化外部类,也不用使用成员内部类的方式调用,直接 new 即可
InnerClass innerClass = new InnerClass();
innerClass.test();
}
}
从上面我们可以总结出静态内部类的使用方法和注意事项:
- 静态内部类无法直接访问外部类的非静态成员属性和方法,但是可以通过实例化外部类的方式访问
- 内部类和外部类存在相同名称属性的情况下,默认调用内部类的属性
- 使用静态内部类时不需要通过实例化外部类的方式,直接 new 即可
方法内部类
方法内部类只存在于外部类的方法中,仅在该方法内部可以使用,代码如下:
public class OutterClass {
public String outterName = "OutterClass";
private String outterSecret = "outterSecret";
public static String method = "test";
public Integer age = 2;
public void test() {
// 外部类无法直接调用内部类的属性和方法
// System.out.println(innerName);
class InnerClass{
public String innerName = "InnerClass";
private String innerSecret = "innersecret";
public Integer age = 1;
public void test() {
// 可以直接访问外部类的属性
System.out.println(outterName);
System.out.println(outterSecret);
// 内部类和外部类存在相同名称属性的情况
System.out.println(age);
System.out.println(this.age);
System.out.println(OutterClass.this.age);
}
}
// 实例化和调用内部类
InnerClass innerClass = new InnerClass();
innerClass.test();
}
}
方法内部类不能使用访问修饰符进行修饰,因为他只能在方法内部进行使用
匿名内部类
匿名内部类是我们开发的时候使用的最多的一种,也是可以讲的最多的一种。他适合那种只创建一次的使用场景。
首先,我们来看两段实现的代码:
// 基于抽象类的匿名内部类实现方式
abstract class Animal{
public abstract void bark();
}
public class Test{
public static void main(String[] args) throws InterruptedException {
// 这个不叫匿名内部类,因为有 animal 这个名字
Animal animal = new Animal() {
@Override
public void bark() {
// TODO Auto-generated method stub
System.out.println("bark");
}
};
animal.bark();
// 这才是匿名内部类,符合 new + Class + body 的格式
// 我们常用 Thread -> start() 就是这种写法
new Animal() {
@Override
public void bark() {
// TODO Auto-generated method stub
System.out.println("bark bark");
}
}.bark();;
}
}
// 基于接口的匿名内部类实现方式
interface Animal{
public void bark();
}
public class Test{
public static void main(String[] args){
makeBarks(new Animal() {
@Override
public void bark() {
// TODO Auto-generated method stub
System.out.println("bark");
}
});
}
public static void makeBarks(Animal animal) {
animal.bark();
}
}
从上面代码我们总结出下面几点:
- 使用匿名内部类时,我们需要遵循两种写法:
- new + Class + {} + method
- invoke( new + Class + {})
- 匿名内部类中不能定义构造函数(初始化工作使用构造代码块完成);
- 匿名内部类中不能存在任何静态成员变量和静态方法;
- 匿名内部类是方法内部类的延伸,所以方法内部类的所有限制同样生效;
- 匿名内部类中必须实现继承的类或者实现的接口的所有抽象方法;
- 内部类中使用的形参必须为 final 。
原因(参考博客)
同时在这个例子,留意外部类的方法的形参,当所在的方法的形参需要被内部类里面使用时,该形参必须为final。这里可以看到形参name已经定义为final了,而形参city 没有被使用则不用定义为final。为什么要定义为final呢?在网上找到本人比较如同的解释:
“这是一个编译器设计的问题,如果你了解java的编译原理的话很容易理解。
首先,内部类被编译的时候会生成一个单独的内部类的.class文件,这个文件并不与外部类在同一class文件中。
当外部类传的参数被内部类调用时,从java程序的角度来看是直接的调用例如:
public void dosome(final String a,final int b){
class Dosome{public void dosome(){System.out.println(a+b)}};
Dosome some=new Dosome();
some.dosome();
}
从代码来看好像是那个内部类直接调用的a参数和b参数,但是实际上不是,在java编译器编译以后实际的操作代码是
class Outer$Dosome{
public Dosome(final String a,final int b){
this.Dosome$a=a;
this.Dosome$b=b;
}
public void dosome(){
System.out.println(this.Dosome$a+this.Dosome$b);
}
}}
从以上代码看来,内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。
这样理解就很容易得出为什么要用final了,因为两者从外表看起来是同一个东西,实际上却不是这样,如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性,因为从编程人员的角度来看他们是同一个东西,如果编程人员在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种尴尬的问题存在,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在。”
(简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变)
继承
继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。因此继承需要符合的关系是:is-a,父类更加通用,子类更具体。
那么为什么需要继承呢?主要目的就是解决代码的冗余性,实现代码复用。
语法规则:class 子类 extends 父类
继承的特性
1、我们知道 Java 不支持多继承,但是支持多重继承。
2、子类拥有父类非 private 的属性、方法;
3、子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;
4、子类可以用自己的方式实现父类的方法(override,重写不是重载,和overload不同),调用时优先调用子类中的该方法
方法的重写
在继承中,子类会自动继承父类的属性和方法。但是如果子类需要对父类的方法修改,以实现不同的业务逻辑的时候,这时候就需要方法的重写(override)。当调用方法时,会优先调用子类的方法。如下面的代码中:
abstract class Animal{
abstract void bark();
}
class Person extends Animal{
@Override
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello !");
}
}
class Man extends Animal{
@Override
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello man!");
}
}
class Woman extends Animal{
@Override
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello woman!");
}
}
class Dog extends Animal{
@Override
void bark() {
// TODO Auto-generated method stub
System.out.println("WAH WAH !");
}
}
重写时需要注意,方法的返回值、方法名、参数类型及个数必须和父类相同,才叫方法的重写。By the way,有些人说必须是方法签名相同,这边普及一下方法签名是什么。Effective Java 书中说的是方法签名由两部分组成:方法的名称和参数类型。Java 中不允许出现方法签名一样的方法,因此,下面这段代码是无法通过编译的:
public int A(){}
public long A(){}
有些人在说 override 的时候喜欢说是方法的重载,在中文里面感觉差不多,但是翻译过来还是不一样的。重载是(overload),重写是(override)。重写时在同一个类中实现方法名相同,参数不同的方法,两个方法的签名是不同的。重写是实现子类中对父类存在的方法进行修改的方法,子类和父类的方法签名相同。
初始化
这个我们在关键字 static 中讲过。
存在继承的情况下,初始化顺序为:
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
继承相关关键字
1、extends
这个关键字用于声明类的继承
2、fianl
使用 final 进行修饰,有一种不可变的意思。
- 修饰类,则表示这个类不能被继承
- 修饰方法,则表示这个方法不能被重写
- 修饰属性,则表示该属性只能够被赋值一次,一旦被赋值就不可以再被改变(对基本类型来说,是值不能被改变,对引用类型来说是引用对象不能改变)。属性的值初始化可以在两个地方:一是在属性定义时直接赋值,二是在构造函数中
3、super 和 this
super 用于在子类中使用父类的方法和属性:
- super.property 访问父类的属性
- super.method 访问父类的方法
- super() 使用父类的构造方法(PS:在子类的构造方法中,我们需要调用父类的构造方法。如果我们没有显式调用父类的构造方法,那么系统默认调用父类无参构造方法。如果父类没有无参构造方法,那么编译会出错)
this 用于类中表示这个实例的意思。
Object 类
Object 类是所有类的父类,如果一个类没有明确表示集成另一个类,那么这个类默认继承 Object 类。因此,可以将 Object 类视为所有类的父类,Object 类中的方法在所有类中都能使用。
下面来看看 Object 类中的方法:
1、native 方法 registerNatives()
作用是在加载的动态文件中定位并连接 native 方法。该方法在代码块中,在类加载时就已经被执行。
2、Object()
无参构造方法
3、native 方法 getClass()
获取类信息
4、native 方法 clone()
返回当前实例的一个副本
5、equals()
返回两个对象地址的比较。重写 equals 方法以进行属性的比较。
6、finalize()
销毁对象,由GC调用
7、hashCode()
返回 hash 值
8、notify()
唤醒一个以当前实例为锁的线程
9、唤醒所有线程
唤醒所有以当前实例为锁的线程
10、toString()
返回类名 + hash 码。对其进行重写以返回实例的属性值
11、wait()、wait(long,int)、wait(long)
当前线程进入休眠直到被其他线程唤醒或者直到一定时间之后
多态
多态是同一个行为具有多种不同表现形式或者形态的能力,或者对象的多种形态的能力。
Java 中的多态主要表现在两个方面:引用的多态、方法的多态
引用多态
引用多态其实就是一句话:父类的引用可以指向父类的对象,父类的引用可以指向子类的对象。
比如下面一段代码:
class Person{
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello !");
}
}
class Man extends Person{
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello man!");
}
}
class Woman extends Person{
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello woman!");
}
}
public class Test{
public static void main(String[] args) {
//父类引用可以指向父类对象,父类引用也可以指向子类对象
Person person = new Person();
Person man = new Person();
Person woman = new Person();
invoke(person);
invoke(man);
invoke(woman);
// 子类对象可以放到父类形参的方法中
Man newMan = new Man();
Woman newWoman = new Woman();
invoke(newMan);
invoke(newWoman);
}
public static void invoke(Person person) {
person.bark();
}
}
方法多态
创建父类对象时,调用的方法是父类的方法,调用子类对象时,调用的方法是子类的方法。
就如上面的代码中
//父类引用可以指向父类对象,父类引用也可以指向子类对象
Person person = new Person();
Person man = new Person();
Person woman = new Person();
invoke(person);
invoke(man);
invoke(woman);
// 子类对象可以放到父类形参的方法中
Man newMan = new Man();
Woman newWoman = new Woman();
invoke(newMan);
invoke(newWoman);
输出结果为:
Say hello !
Say hello !
Say hello !
Say hello man!
Say hello woman!
引用类型转换
引用类型转换分为:向上转换和向下转换。
向上转换
向上转换是子类到父类的转换,如之前的代码
Person person = new Person();
Person man = new Person();
Person woman = new Person();
invoke(person);
invoke(man);
invoke(woman);
这种转化没有风险。
向下转换
向下转换是从父类到子类的转换,如下面的代码:
Person man = new Person();
Person woman = new Person();
Man newMan = (Man)man;
Woman newWoman = (Woman)woman;
这样的转换需要在对象面前加上 (目标Class) 进行强制类型转换,因此有数据溢出的风险。
但是,下面的代码虽然编译能通过,但是运行时会报错:
Man man = (Man)new Person();
Woman woman = (Woman)new Person();
instanceof 和 getClass 的使用
为了避免类型转换的安全问题,这里可以使用 instanceof 和 getClass 的方法进行类型判断。但是 instanceof 是指某个对象是一个xx类的实例,比如说man instanceof Person
是可以的,但是 getClass 不考虑继承关系,man.getClass == Person.class
就不行了。
抽象类
抽象类是被 abstract 关键字修饰的类,它有以下特点:
1、抽象类不能被实例化
抽象类不能被直接创建,但是可以定义引用变量来指向子类对象。代码如下:
abstract class Person{
abstract void bark();
}
class Man extends Person{
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello man!");
}
}
class Woman extends Person{
void bark() {
// TODO Auto-generated method stub
System.out.println("Say hello woman!");
}
}
public class Test{
public static void main(String[] args) {
// Person person = new Person(); // 不能被实例化
Person man = new Man();
Person woman = new Woman();
}
}
2、抽象类只约定了子类必须有什么方法,但不关心子类如何实现这些方法。抽象类中可以包含普通的方法,也可以有抽象方法。
3、抽象类中如果有抽象方法,只有声明,没有实现。子类中必须实现对抽象方法的重写,否则会报错。
接口
1、概念
接口可以理解为一种特殊的类,由全局常量和公共的抽象方法所组成。也可理解为一个特殊的抽象类,因为它含有抽象方法。
如果说类是一种具体实现体,而接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部数据,也不关心这些类里方法的实现细节,它只规定这些类里必须提供的某些方法。(这里与抽象类相似)
2、基本语法
[修饰符] [abstract] interface 接口名 [extends父接口1,2....](多继承){
0…n常量 (public static final)
0…n 抽象方法(public abstract)
}
其中[]里的内容表示可选项,可以写也可以不写;接口中的属性都是常量,即使定义时不添加 public static final 修饰符,系统也会自动加上;接口中的方法都是抽象方法,即使定义时不添加 public abstract 修饰符,系统也会自动加上。
3、使用接口
一个类可以实现一个或多个接口,实现接口使用 implements 关键字。java 中一个类只能继承一个父类,是不够灵活的,通过实现多个接口可以补充。语法为:
[修饰符] class 类名 extends 父类 implements 接口1,接口2...{
类体部分 //如果继承了抽象类,需要实现继承的抽象方法;要实现接口中的抽象方法
}
注意:如果要继承父类,继承父类必须在实现接口之前,即 extends 关键字必须在 implements 关键字前
接口和抽象类的区别
首先,这两者的语法区别如下:
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用 abstract 关键字声明 | 接口使用 interface 关键字声明 |
实现 | 子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话,他需要提供抽象类中所有声明的方法的实现 | 子类使用 implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口没有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是 public,并且不允许定义为 private 或者 protected |
多继承(实现) | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final |
设计 | 抽象类是模板类设计 | 接口是契约式设计 |
作用 | 抽象类被继承时体现的是 is-a 关系 | 接口被实现时体现的是 can-do 关系 |
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样的原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类;
- 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能
备注:Java 8 中接口引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。