一、封装
1.1 封装简介
1)从广义的角度上来说
封装就是将一块要使用的代码片段,定义到方法中。
将多个方法和多个状态数据定义到类体中,也是一种封装。
2)从狭义的角度上来说
封装就是把类里面的属性私有化(用private修饰),再通过公有方法(public)进行访问和修改。
1.2 属性封装
1)为什么要封装?
一个类中的某一些属性,不希望直接暴露给外界,让外界直接操作。因为如果让外界直接操作的话,对 这个属性进行的值的设置,可能不是我们想要的(可能不符合逻辑)。此时就需要将这个属性封装起来,不让外界直接访问。
2)如何封装?
1.添加关键字private访问权限修饰词修饰属性,外界就不能直接访问属性了。
2.然后提供公有的getter/setter方法,来操作这个被私有化的属性。
3)为什么私有化起来的属性,不希望外界访问,还需要再提供setter和getter?
可以通过指定的方式访问属性,在这些方法中,可以添加一些数据处理操作。
public class APerson {
//成员变量私有化
private String name;
private int age;
private char gender;
//无参构造器
public APerson(){}
//全参构造器
public APerson(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
//为每个成员变量,提供getter/setter方法
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setAge(int age){
if(age > 0&& age <= 120) {
this.age = age;
}else{
System.out.println("--您赋值的年龄不合法--"+age);
}
}
public int getAge(){
return age;
}
public void setGender(char gender){
if(gender == '男' || gender == '女'){
this.gender = gender;
}else{
System.out.println("--您赋值的性别不合法,需要汉字'男'或'女'--");
//如果赋值有问题,可以使用下面的方式让程序中断,并提示异常
throw new RuntimeException("--您赋值的性别不合法,需要汉字'男'或'女'--");
}
}
public char getGender(){
return gender;
}
}
测试:
public class APersonTest {
public static void main(String[] args) {
//创建对象
APerson p1 = new APerson();
//p1.name = "小明"; name私有化了,就不可以直接访问
//调用公用方法去访问
p1.setName("小明");
p1.setAge(18);
p1.setGender('女');
//System.out.println(p1.name); 也不能直接访问,需要调用公有的方法
System.out.println(p1.getName());
System.out.println(p1.getAge());
System.out.println(p1.getGender());
p1.setGender('m');
System.out.println(p1.getGender());
}
}
1.3 单例设计模式
1.3.1 单例的饿汉模式
1. 提供一个该类的,私有的(private),静态的属性, 并在静态代码块里赋值
2. 提供一个私有的构造器, 避免外界直接调用构造器。
3. 提供一个公有的(public),静态的方法,来获取当前类的对象。
public class BSingleton {
private static BSingleton instance;// instance 实例的含义
static{
instance = new BSingleton();
}
private BSingleton(){}
public static BSingleton getInstance(){
return instance;
}
}
测试:
public class BSingletonTest {
public static void main(String[] args) {
//看是否能直接创建
//BSingleton b = new BSingleton(); 构造器私有化,不能直接调用
//看是否能直接访问成员
//BSingleton.instance; //成员私有化了,不能直接访问
//只能使用下面的方式获取该类的具体实例
BSingleton bs = BSingleton.getInstance();
//再获取一次
BSingleton bs2 = BSingleton.getInstance();
/**
* ==: 双等号,是用来判断引用类型的变量里的地址是否相同
*/
System.out.println(bs==bs2);
}
}
true
1.3.2 单例模式的懒汉模式
1.提供一个该类的,私有的(private),静态的属性。
2.提供一个私有的(private)构造器,避免外界直接调用构造器。
3.提供一个公有的(public),静态的方法,来创建当前类的对象,如果对象不存在,说明还没创建对象,那么第一次调用时会创建一个对象,后续调用则直接返回已存在的对象。
public class CSingleton {
//私有化的静态变量
private static CSingleton instance;
//私有化构造器
private CSingleton() {}
//公有的静态方法,用于获取对象
public static CSingleton getInstance() {
//如果 静态变量里面没有地址,说明还没有创建对象
if (instance == null) {
//创建对象,将地址存入静态变量
instance = new CSingleton();
}
//返回之前的对象,如果之前有就用之前的,如果没有,就用新的
return instance;
}
}
public class CSingletonTest {
public static void main(String[] args) {
//获取两次该类型的对象
CSingleton c1 = CSingleton.getInstance();
CSingleton c2 = CSingleton.getInstance();
//使用 双等号,判断一下,c1和c2指向的是不是同一个对象
System.out.println(c1 == c2);
}
}
true
1.3.3 两者比较
基本上是一样的,都可以获取到一个类的唯一对象。
1. 在没有使用获取当前类对象之前,懒汉式单例比饿汉式单例在内存上有较少的资源占用。
2. 单例懒汉模式存在线程安全隐患问题,比如两次调用的时候都恰好执行到判断等于null。那么都会执行到创建实例那一行代码。那么就会在内存中创建出来两个对象。
二、继承
2.1 继承的简介
继承是面向对象最显著的一个特征。继承是从已经有的类中派生出新的类,新的类能继承已有类的所有数据属性和行为,并且还可以扩展新的属性和行为。
已有的类称为父类,又叫基类,超类。派生出来的新的类,叫子类,也叫派生类。
使用关键字extends来表示子类继承了父类。
语法:修饰词 class 子类名 extends 父类名{
//子类的类体
}
2.2 继承的特点
1. Java只直接单继承,即一个类只能有一个父亲,一个子类只能继承一个父类;但一个父亲可以有多个孩子,即一个父类可以被多个子类继承。
2. Java支持多重继承,即一个子类在继承一个父类的同时,还可以被它的子类所继承。可见继承具有传递性。
3. 子类继承了父类的所有成员变量和方法,包括私有的(只不过没有访问权限),当然也包括静态成员。
4. 子类不会继承父类的构造方法,只能调用父类里的构造方法,并且一定至少有一个子类构造器调用了父类的构造器。
5.子类在拥有父类的成员基础上,还可以添加新成员。
2.3 继承中的构造器
定义一个Person父类构造器如下:
public class APerson {
private String name;
private int age;
private char gender;
public APerson(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public APerson(){}
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 char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
}
特点:1. 子类不能继承父类的构造器。
2. 子类的构造器中至少存在一个构造器调用了父类的构造器。默认调用的是父类中的无参构造器。如果父类中没有无参构造器,那么子类需要显示调用父类中的某一个有参构造器。
3. 子类的构造器中可以使用super(有参传参)进行显示,调用父类中的某一个构造器。
4. super(有参传参)与this(有参传参)一致,必须放在首行首局,因此不能共存。
5. 为什么要至少有一个调用了父类的构造器,因为子类继承过来的属性需要初始化。
public class Student extends APerson{
private String studentId;
private String classNo;
//添加自己的构造器
public Student(String studentId, String classNo) {
super("小黑",10,'女');//首行首句
this.studentId = studentId;
this.classNo = classNo;
}
public String getClassNo() {
return classNo;
}
public void setClassNo(String classNo) {
this.classNo = classNo;
}
public String getStudentId() {
return studentId;
}
public void setStudentId(String studentId) {
this.studentId = studentId;
}
//重载一个构造器
public Student(String name,int age,char gender,String studentId,String classNo){
//this.name = name; 父类里的name属性私有化,虽然子类继承了,但是不能直接访问
//setName方法是继承过来的,并且是公有的,因此是可以直接使用的
this(studentId,classNo);
setName(name);
setAge(age);
setGender(gender);
}
//添加toString方法,用来显示对象的属性值。
public String toString(){
return "name:"+getName()
+",age:"+getAge()
+",gender:"+getGender()
+",studentId:"+getStudentId()
+",classNo:"+getClassNo();
}
}
2.4 继承中的方法重写
重写,叫做Override。在子类中,对从父类中继承到的方法进行重新的实现。这个过程中,子类重写该方法,会覆盖掉继承自父类中已经实现的方法。因此,重写又叫做覆写。
为什么要重写?
因为父类的方法逻辑不能满足子类的需求了,因此子类需要修改逻辑(重写)。
重写的特点:1. 子类只能重写父类中存在的方法。
2. 重写时,子类中的方法名和参数要与父类保持一致。(与重载overload不同)。
3. 返回值类型:必须和父类方法的返回值类型相同,或者是其子类型。
4. 访问权限:子类重写方法的访问权限必须大于等于父类方法的访问权限。
注解@Override:这个注解,用在重写的方法之前,表示验证这个方法是否是一个重写的方法。如果是,程序没有问题。如果 不是,程序会报错。因为我们再进行方法的重写时,是没有什么提示的,因此,为了保证写的是重写,在重写之前,最好加上这个注解。
加了@Override就是重写,没有加@Override就不是重写。 这种说法是错误的! @Override只是进行的一个语法校验,与是不是重写无关。
Override 和 Overload 的区别:
Override: 是重写,是子类对父类的方法进行重新实现。
Overload: 是重载,是对同一个类中的同名、不同参数方法的描述。
public class TDog extends Animal {
public static void main(String[] args) {
//创建一个子类型对象
TDog dog = new TDog();
//调用继承过来的并且有访问权限的方法
dog.sport();
}
//子类dog独有功能
public void noise() {
System.out.println("---汪汪汪---");
}
//子类将父类的方法完全一模一样的写出来就是重写的一种
public void sport(){
System.out.println("--运动中--");
}
//子类在重写父类的方法时,返回值类型与父类中的方法可以相同,也可以是其子类型:TDog就是Animal的子类型
@Override
public TDog getMyClass(){
return null;
}
//子类在重写父类的方法时,访问权限应该大于等于父类方法的访问权限
@Override
public String showInfo(){
return null;
}
//自己独有的方法,只不过是与继承过来的那个是重载关系
public String showInfo(int a){
return null;
}
}
//动物类型
class Animal{
private String color;
//公有的,返回值类型void
public void sport(){
System.out.println("--运动中--");
}
//公有的,返回值类型Animal
public Animal getMyClass(){
return null;
}
//默认的,返回值类型String
String showInfo(){
return null;
}
}
2.5 Object类型
Object类,是Java中的根类。所有的类都直接或者间接的继承自Object类。 因为,所有的类都直接或者间接的继承自Object类,因此在Object类中定义的属性、方法,在所有的类中都有包含。 比如常用的方法 hashCode(),equals(),toString(),getClass(),wait(),notify(),notifyAll()等。
1)toString()方法
源码如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
1. 该方法的作用是将对象的属性信息拼接起来,返回字符串。这样就可以查看每个对象的样子了。 2. Object里提供的该方法的逻辑:返回的是类全名+@+hashCode值的16进制,对于我们来说 没有太大意义,看不到对象的成员变量的信息。因此自定义类型时,通常都要重写。 3. 该方法在使用输出语句打印对象的变量时,会自动调用。
2)equals()方法
源码如下:
public boolean equals(Object obj){
return this==obj; // == 比较的是地址值。
}
上述的源码的意义所在:比较this和传进来的obj 是不是同一个对象,如果是,则返回true,不是的话,返回false。
而我们大多时候的需求,并不是比较是不是同一个对象,而是比较两个对象的属性值是否相同。 因此:自定义类型时,应该重写eqauls方法。
重写要遵循一些原则:
1. 判断一下传进来的是不是null,不是,返回true。
2. 判断一下传入的是不是自己,是,返回true。
3. 判断一下是不是同类型,如果不是,就转成同类型进行比较。
4. 其他任何情况,都返回false。
5. 如果 a.equals(b) 成立,则 b.equals(a) 也必须成立。
6. 如果 a.equals(b), b.equals(c) 成立,则 a.equals(c) 也必须成立。
3)hashCode()方法
该方法返回的是一个int类型的值。 表示对象在内存堆中的一个算法值。 在自定义类型时,一般都需要重写该方法。
@Override
public int hashCode() {
return Objects.hash();
}
4)getClass()方法
方法原型 : public final native Class<?> getClass();
方法作用 : 获取一个用来描述指定类型的Class类对象。获取一个指定的对象的类型。 这个方法,不能被重写。
public class Teacher {
private String name;
private int age;
private char gender;
public Teacher() {
}
public Teacher(String name, char gender) {
this.name = name;
this.gender = gender;
}
public Teacher(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof Teacher) {
Teacher t = (Teacher) obj;
return this.name.equals(t.name) && this.age == t.age && this.gender == t.gender;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(name, age, gender);
}
@Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
'}';
}
public static void main(String[] args) {
Teacher teacher = new Teacher();
System.out.println(teacher);
Teacher t1 = new Teacher("michael",39,'男');
Teacher t2 = new Teacher("michael",39,'男');
System.out.println(t1==t2);
boolean r = t1.equals(t2);
System.out.println("r:"+r);
//通过对象调用getClass(),来获取Teacher对应的描述类的对象
Class<? extends Teacher > aClass = t1.getClass();
System.out.println(aClass.getName());
//获取类里的所有属性: Field 字段的含义。
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
}
}
Teacher{name='null', age=0, gender= }
false
r:true
com.oop.day02._02Extend.Teacher
name
age
gender
2.6 final修饰词
final:最终,最后的含义。
final是java语法中的修饰词,可以用来修饰类,属性,方法,局部变量。
1. 修饰类时:表示该类不能再有子类,即不能被继承。
2. 修饰属性(成员变量)时:只能初始化一次,即要么直接初始化,要么在构造器中初始化,不能被再次赋值。
3. 修饰方法时:表示该方法不能被重写。
4. 修饰局部变量时,只能赋值一次,不能第二次赋值。
2.7 static修饰词
static修饰的内容,都是属于公有资源,不属于对象,而是属于类的,因此都是类名调用。
1. 修饰成员变量:静态成员变量,属于类的,公共资源,使用类名.调用。
注意:可以使用引用变量.调用,但是不合理,因为不属于对象。
因此,static修饰的属性,应该配合 final 一起修饰,来表示该属性是一个常量。
常量: 命名时,字母都大写,多个单词使用下划线连接。应该使用 public static final修饰。
2. 修饰方法: 静态方法,属于类的,公有资源,使用类名.调用。
注意:可以使用引用变量.调用,但是不合理,因为不属于对象。
静态方法中不能直接访问非静态成员。
static方法不能被重写,但是子类和父类可以提供和父类一样的静态方法,各是各的。
3. 修饰代码块: 静态代码块, 类加载时只执行一次。通常用于加载静态资源:图片,音频等。
4. 修饰类: 可以修饰内部类。
public class MyUtil {
private String name;
//定义一个水桶的容量,单位是升
public static int contain = 18;
//如果想要达到每时每刻看到的公有资源都是一样的,那么就应该使用final修饰,即常量。
public static double PI = 3.14;
public static void sum(int a, int b) {
//不能直接访问非静态成员
//this.name = "aaaa";
//addContain();
contain = 19;
}
public void addContain() {
this.contain++;
}
public void subContain() {
this.contain--;
}
public static void main(String[] args) {
MyUtil p1 = new MyUtil();
//查看容量,使用了变量调用,但是不建议
System.out.println(p1.contain);
p1.addContain();
MyUtil p2 = new MyUtil();
//注意:静态变量要使用类名.调用
System.out.println(MyUtil.contain);
p2.subContain();
}
}
class Sub extends MyUtil {
//@Override 添加注解后报错,因为静态方法不能被重写
public static void sum(int a, int b) {
//不能直接访问非静态成员
//this.name = "aaaa";方法不能被重写,但是子类和父类可以提供和父类一样的静态方法,各是各的。
//addContain();
contain = 19;
}
}
三、多态
3.1 简介
多态:从字面上理解,就是多种形态,多种状态的含义,在这里,指的是一个对象具有多种形态的特点。说的再简单点,就是一个对象可以从一种类型转换为另外一种类型。有向上转型和向下转型两种形式。
public class Animal {
private String name;
private String color;
private int age;
public Animal() {
}
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
//动物都会有发出声音的行为
public void noise() {
System.out.println("---动物都会发出叫声---");
}
}
class Dog extends Animal {
public void noise() {
System.out.println("---汪汪汪---");
}
public void lookHouse() {
System.out.println("---会看家---");
}
public Dog(String name, String color, int age) {
super(name, color, age);
}
}
class Cat extends Animal{
public void noise(){
System.out.println("---喵喵喵---");
}
public void getMouse(){
System.out.println("---我会抓老鼠---");
}
public Cat(String name, String color, int age) {
}
}
3.2 向上造型(向上转型)
1. 父类型的变量引用子类型的变量。
2. 向上转型肯定会成功,是一个隐式转换。
3. 向上转型后的对象,将只能够访问父类中的成员(编译期间,看变量类型)。
4. 如果调用的是重写过的方法,那么调用的一定是重写方法(运行期间,看对象)。
5.应用场景:在定义方法时,形式参数是父类型的变量。这样更加灵活,可以传任意子类型对象。
public class AinmalTest {
public static void main(String[] args) {
//使用Animal声明一个变量,引用一个Cat对象
Animal a = new Cat("小花","白色",3);
//调用方法:
a.noise();//编译期间不会出现问题。因为父类里有该方法。运行期间,执行的是对象的类型里的方法逻辑。
//a.getMouse();调用不到该方法,因为a这个变量的类型里没有该方法,(编译期间,看变量类型)
test1(a);
Dog dog = new Dog("大黑","黑色",8);
test1(dog);
}
//测试:执行动物叫声 这就是向上造型的优势所在,父类型的变量作为参数,更加灵活,可以传入不同的子类型对象。
public static void test1(Animal animal){
animal.noise();
}
}
---喵喵喵---
---喵喵喵---
---汪汪汪---
3.3 向下转型
1. 父类型变量赋值给子类型的变量,需要强制转换,是一个显示转换。
2. 可能会失败,失败的话,会报类造型异常ClassCastException。
3. 为了避免ClassCastException,可以使用instanceof来判断:变量指向的对象是否属于某一个类型。
4. 为什么要向下转型:前提,父类型的变量引用子类型的变量,但是父类型的变量调用不了子类型中独有的功能,已经不能满足需求。
5. 应用场景:多数情况下,定义方法时,形参都是父类型的变量,原因是可以接受任意子类型对象。 但是有的时候,在这样的方法里的逻辑中,可能会用到该子类型对象的独有功能,因此会涉及到向下转型。
语法:子类型 变量名 = (子类型)父类型变量
public class AnimalTest2 {
public static void main(String[] args) {
//创建一个父类型的变量引用子类型的变量
Animal am = new Dog("大黄","黄色",5);
am.noise();
//am.lookHouse(); 编译期间看变量类型。没有该方法,所以报错。
//只能向下转型才可以
Dog d = (Dog)am;
d.lookHouse();
//上述代码没有问题,但是在正在编程时,有可能写成如下代码:强制转换的类型不正确
//编译期间不报错。但是运行期间就会报异常。
// Cat c = (Cat)am;
// c.getMouse();
//我们应该避免上述情况发生。只需要使用instanceof即可
if(am instanceof Cat){//因为am指向的是一个Dog对象,不属于Cat类型,因此进不去分支。所以避免了报错。
Cat c = (Cat)am;
c.getMouse();
}
}
}