08.三大特性
封装
、继承
和多态
是面向对象编程的三大特性
封装
现有一个Student类,里面有成员变量 考试分数score
,如果我们能通过new一个对象对其分数进行直接修改,那tm还能行吗?!封装的目的就是为了保证变量的安全性,使得用户只能通过外部接口(见多态)访问类的成员,而不能直接对实例变量进行查看与修改。讲的更明白些,就是把类的属性私有化,再通过公有方法进行访问和修改
public class Student{
private String name;
private int score; //无法直接访问修改
public Student(String name, int score){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public int getScore(){
return score; //通过公有方法对私有属性进行访问
}
}
封装使得方法成为一个“黑箱”,只需知道其作用不用知道其实现细节,在修改属性名时,不会影响外部接口对属性的访问
//修改前
private String name;
public String getName(){
return name;
}
//修改后
private String firstname;
private String lastname;
public String getName(){
return firstname + lastname; //仅改变方法的实现
}
另外,如果用户通过公有方法访问属性时按一定的规矩办事或者得到一些提示,通过封装可以实现这些小操作。比如说你不能在更改年龄时输入250岁,这显然不可能
private String name;
private int age;
public setName(String name){
System.out.println("请输入你的姓名:"); //提示
this.name = name;
}
public setAge(int age) throw Exception{
if(age < 0 || age > 150){
throw new Exception("年龄不符合规范"); //抛出异常,避免非法的输入
}
this.age = age;
}
继承
在定义了不同类的时候可能会存在一些相同的属性和方法,为了代码复用、减少重复工作可以把这些共同属性抽象成一个父类。比如说,Student和Worker都拥有姓名、年龄等属性和吃饭、睡觉等方法,为了减少代码的重复定义,可以把这些抽象成一个Person类,因为人都是有姓名年龄、能够吃饭睡觉的,这些都是人的共性。此时就可以把Student和Worker称作子类,Person称作父类。再比如Cat、Dog、Pig和Animal的关系。子类可以复用父类非私有的方法和属性,且可在此基础上新增方法和属性
每一个子类必须定义一个实现父类构造方法的构造方法,即在构造方法开始使用super
关键字,如果父类使用的是默认构造方法,那么子类就不用手动声明
//文件:Person.java
public class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
}
//文件:Student
public class Student extends Person{ //通过extends关键字来继承父类
public Student(String name, int age){ //实现父类构造方法的构造方法
super(name,age); //通过super关键字(指代父类)实现父类的构造方法
}
public void learn(){
System.out.println("我好喜欢学习啊!"); //子类特有的方法
}
}
//文件:Worker
public class Worker extends Person{
public Worker(String name, int age) {
super(name, age);
}
public void work(){
System.out.println("劳动最光荣!");
}
}
需要注意的是,Java是单继承语言,每个子类都只有一个父类;如果一个类没有继承任何类,那么这个类默认继承Object类。当然,即使继承自某个类,但最顶层的父类仍然是继承自Object类的。
多态
从字面意思讲,多态就是多种形态,在Java中指一个类有多种表现形态,即一个方法由于实现类的不同而导致执行的结果也不同
方法的重写
方法的重写与重载不一样,重写指对原有方法的覆盖。子类可以提升父类方法的访问权限,但private修饰的方法和静态方法不能被重写
//文件:Person.java
public class Person{
public void eat(){
System.out.println("我吃饭了");
}
}
//文件:Student.java
public class Student extends Person{
@Override //声明这个方法是重写的,不过可以省略,但是不建议
public void eat(){
System.out.println("真香");
}
public void learn(){
System.out.println("学习真好玩");
}
}
如果在重写方法时,不仅想使用子类自己的逻辑,也想直接调用父类的方法或变量,可以用super
关键字访问
public void eat(){
super.eat(); //调用父类的方法
System.out.println("真香"); //子类的逻辑
} //输出为:我吃饭了 真香
类型转换
存在亲缘关系的类之间也可以进行类型转换,可以分为向上转换和向下转换两种。当向上转型之后,父类引用变量可以访问子类中属于父类的属性和方法,但是不能访问子类独有的属性和方法。只有当对象原本就是通过向上转型得到的时候,才能进行向下转型
//向上转型
Person person = new Student("阿伟",20); //父类变量引用子类实例
person.eat(); //得到具体实现(Student)的结果,而不是当前类型(Person)的结果 输出为:真香
person.learn(); //报错,因为learn方法是子类特有的方法。
//向下转型
Person person = new Student("阿伟",20); //通过子类向上转型创建的对象
Student student = (Student)person; //将原本的Person类向下转型成具体的子类Student
student.eat(); //调用具体实现类的方法
instanceof关键字
instanceof
是Java的一个保留关键字,返回值类型是布尔值Booleam
,格式为 对象 instanceof 类
,能够判断左边的对象是不是右边类或其子类的具体实现
//现有一个Person类型的实例person,要判断其是Student类还是Worker类的实现(假设Person类仅有这两个子类)
private static void test(Person person){
if(person instanceof Student){
Student student = (Student)person;
student.learn();
}
else if(person instanceof Worker){
Worker worker = (Worker)person;
worker.work();
}
}
final关键字
final一共有四个用法
- 修饰类:这个类不能有子类(太监类),类中所有的成员方法不能被重写。
- 修饰成员方法:该方法为最终方法,不能被重写。
- 修饰局部变量:对于基本类型来说,该变量中的数据不能改变;对于引用类型来说,该变量中的地址值不能改变。
- 修饰成员变量:由于成员变量具有默认值,所以用final后必须手动赋值;对于final的成员变量,要么直接赋值,要么用构造方法赋 值,只能2选1;必须保证类中所有重载的构造方法都会最终对final的成员变量进行赋值。
private final String name;
private final int age;
public Student(String name, int age){
this.name = name;
this.age = age; //通过构造方法对final修饰的成员变量进行初始化
}
抽象类
抽象类从字面意思上讲就是比类更抽象的类,在普通类中增加抽象方法就成了抽象类,需要用abstract
关键字去声明。抽象方法是没有方法体的方法,不能被对象直接使用,同样用abstract
关键字去修饰
public abstract Person{ //定义一个抽象类
public abstract void eat(); //无方法体,为抽象方法
public void talk(){
System.out.println("说话"); //有方法体,为普通方法
}
}
抽象类可以保留特征,但不保留具体呈现形态,方法由非抽象的子类去实现,子类必须对所继承抽象类中所有的抽象方法进行重写,且创建实例时需使用向上转型处理。另外,抽象类是单继承的,子类只能继承一个父类
public abstract Person{
public abstract void eat(); //定义一个抽象方法
}
class Student extends Person{
@Override
public void eat(){
System.out.println("我在吃饭"); //子类对父类抽象方法的重写
}
}
public class Test{
public static void main(String[] args){
Person person = new Student(); //向上转型
person.eat(); //通过子类实现 输出为:我在吃饭
}
}
接口
接口包含了一系列方法的定义,只有方法的特征而没有其实现,需要类去实现。接口可以理解成代表某个确切功能,比抽象类更加抽象。举个例子,人和狗都有吃
的行为(此处应把人与狗的吃理解成不同的行为,比如人吃饭而狗吃狗粮),甚至对于手机也能把充电
理解成吃
,那么就可以把吃
作为一个接口,通过方法的重写分别在人、狗、手机上具体实现
//文件:Eat.java
public interface Eat{ //通过interface关键字声明一个接口
void eat();
}
//文件:Person.java
public class Person implements Eat{ //通过implements关键字声明实现的接口
@Override
public void eat(){
System.out.println("我是人,我在吃饭"); //对接口中方法的重写
}
}
//文件:Dog.java
public class Dog implements Eat, ...{ //接口能够实现多继承的功能即每个类可以实现多个接口,每个接口之间用‘,’分开
@Override
public void eat(){
System.out.println("我是狗,我在吃狗粮");
}
}
//文件:Phone.java
public class Phone implements Eat{
@Override
public void eat(){
System.out.println("我是手机,我在充电");
}
}
//文件:Test.java
public class Test{
public static void main(String[] args){
Person person = new Person();
Dog dog = new Dog();
Phone phone = new Phone();
person.eat();
dog.eat();
phone.eat();
}
}
//输出:我是人,我在吃饭 我是狗,我在吃狗粮 我是手机,我在充电
接口中的变量默认为 public static final ;另外,一个类可以实现多接口并不是指多继承;实现接口的类能通过Instanceof关键字判断,支持向上和向下转型。
09.内部类
定义在类内部的类叫作内部类,可分为四种:成员内部类、静态内部类、局部内部类和匿名内部类。
成员内部类
类中直接嵌套的类是成员内部类,是属于对象的,因此要创建内部类的对象,必须要先创建外部类的对象
public class Test{
class Inner{
}
public static void main(String[] args){
Test test = new Test(); //先创建外部对象
Inner inner = test.new Inner(); //通过外部对象创建内部对象
}
}
静态内部类
静态内部类是属于类的,因此不需要像上面这么麻烦,可以直接通过类名.
访问,省去了创建对象的过程
public class Test{
static class Inner{
}
public static void main(String[] args){
Test.Inner inner = new Test.Inner(); //通过类名.直接访问
}
}
局部内部类
public class Test{
public void test(){
class Inner{
}
Inner inner = new Inner();
}
}
匿名内部类
如果一个接口/类的方法在程序中只需执行一次,为此创建其实现类或子类并重写方法是非常不值的,此时可以用匿名内部类在new的时候直接写对应的实现,减少冗余代码。缺点也很明显,仅限在这里使用,不能够代码复用。另外,可以借助 lambda表达式
实现简化
public static void main(String[] args){
Eat eat = new Eat(){
@Override
public void eat(){
//重写的方法
}
}; //此处‘;’不能忘记!本质是Eat eat = new Eat();
}
//lambda表达式简化格式:
public static void main(String[] args){
Eat eat = () -> {};
}
10.枚举类
枚举类是指对象有限且固定的类,默认继承Enum类,可以使用Enum类的方法,通过关键字enum
定义。举个例子,某个程序需要你传入今天是星期几,照理来说你得按照他的要求输入MON、TUE、…,但也不排除有一些大聪明非得输入个 “星期2” ,这样就显得不严谨了。因为星期几是固定的7个,所以需要枚举类。
public enum WeekdayEnum{ //通过enum关键字声明
SUN,MON,TUE,WED,THU,FRI,SAT;
}
非抽象枚举类默认由final
修饰,不能被继承;枚举类的构造方法默认由private
修饰,若使用其他则会报错;所有实例必须在第一行全部列出,代表该枚举类所有可能出现的实例,默认由public static final
修饰。
通常会使用构造方法来进行初始化,而在第一行我们列举出所有的实例时,其实就已经调用了构造方法;一旦默认的无参构造被我们人为定义的有参构造覆盖的话,我们在列举实例时就必须带上对应的参数
//文件:StatusEnum.java
public enum StatusEnum{ //声明一个状态的枚举类
STUDYING("学习"),EATING("吃饭"),WORKING("工作"); //此时就已经调用了有参构造方法
private final String name; //成员变量:状态名
private StatusEnum(String name){
this.name = name; //有参构造,初始化
}
public String getName(){ //成员方法:获取状态,返回状态名
return this.name;
}
}
//文件:Student.java
public class Student{
private String name;
private int age;
private StatusEnum status;
public Student(String name, int age){
this.name = name;
this.age = age;
}
public void setStatus(StatusEnum status){
this.status = status;
}
public StatusEnum getStatus(){
return status;
}
}
//文件:Test.java
public class Test{
public static void main(String[] args){
Student student = new Student("小明",18);
student.setStatus(StatusEnum.STUDYING);
System.out.println(student.getStatus().getName());
}
}
//输出为:学习
11.基本类型包装类
Java中的基本数据类型(int、double、char…)都不是类,如果想通过对象的形式去使用他们,则需要使用基本类型的包装类。包装类命名大部分是由基本类型的首字母变为大写得来的,如Byte -> byte
,Boolean -> boolean
,Integer ->int
,Character -> char
,Long -> long
,Float -> float
,Double -> double
;包装类实际上就是把基本数据类型封装成一个类
private final int value; //Integer内部实际还是存的一个int类型的数据
public Integer(int value){
this.value = value;
}
public static void main(String[] args){
Integer i = 1; //接收参数1,通过构造方法将基本int类型的1变为Integer类型的1
}
此处应该有个自动装箱与拆箱的内容,但我自认为目前能力不足以讲清楚,所以摆烂了。
后记
这期算是比较重要的,因为涉及到面向对象的三大特性,对于初学者来说理解起来有点难度,所以这次我拖得时间有点长(绝对不是因为摆烂),但还是没能细致入微地去介绍,比如Enum类的各种方法。以后可能会出Enum类、ArrayList类等的源码逐行分析,我是说可能,总之有可能。