一.封装
1.1封装的简介
广义上:定义方法属于封装,定义类也属于封装;
狭义上:把类里的属性封装,然后再调用公有的方法去访问和修改;
1.2属性的封装
在属性前面添加private关键字,外界就不可以直接访问属性了,可以提供与属性有关的getter/setter方法来访问属性;
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;
}
首先我们定义了一个Aperson的类,然后把属性都利用private私有化;建立一个无参构造器,一个全参构造器,然后我们需要为每个成员变量都添加getter/setter方法,上述的是使用系统生成的方法;
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;
}
}
我们可以通过getter/setter方法来进行判断传入的实参是否合适,如果不合适就需要让程序中断,在我们调用时,
public class APersonTest {
public static void main(String[] args) {
//创建对象
APerson p1 = new APerson();
// p1.name = "小明"; name 私有化了,就不可以直接访问
//需要调用公有方set方法
p1.setName("小明");
p1.setAge(199);
p1.setGender('k');
//System.out.println(p1.name); 也不能直接访问,需要调用公有方法
System.out.println(p1.getName());
System.out.println(p1.getAge());
System.out.println(p1.getGender());
}
}
首先使用new关键字调用构造器创建一个对象,这里我使用的是无参构造器,现在p1的成员变量的值都是默认值;由于我们把name属性私有化了,所以我们直接使用p1.name = “小明” 这样的方式会报错,我们需要调用公有的静态方法;来设置相关的成员变量,我们如果想获取到p1对象中的属性,也不能直接使用p1.name的格式,而是需要调用getter方法。
1.3单例模式的饿汉模式
提供一个该类的私有的静态的属性,并在静态代码块里赋值
提供一个私有的构造器,避免外界直接调用构造器创建对象
提供一个公有的静态的方法,来获取当前类的对象
public class BSingleton {
private static BSingleton instance;//instance--实例的含义
//类加载期间就创建对象
static{
instance = new BSingleton();
}
//构造器私有化
private BSingleton(){}
public static BSingleton getInstance(){
return instance;
}
根据上述代码,我们先提供一个该类的静态的私有的属性instance;然后在静态代码块中实例化一个对象。把构造器私有化,以免外界调用构造器,最后创建一个公有的静态的getter方法,来访问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);
}
}
我们在测验的过程中,首先看一下能否通过new关键字实例化一个对象,由于构造器私有化,我们无法直接调用,之后看一下创建出的对象instance能否直接访问,成员也被私有化了,无法直接访问,我们只可以通过调用公有的getter方法来获取类的实例,bs和bs2都是一个地址值,我们最后打印一个判断语句,会得到这两个地址值是相同的,说明在这个类中就只实例化了一个对象instance;
1.4单例模式的懒汉模式
提供一个该类的,私有的,静态的属性
提供一个私有的构造器,以防止外界调用构造器实例化对象
提供一个公有的静态的方法来创建对象,如果对象还不存在,说明对象还没有创建
public class CSingleton {
//私有化的静态变量
private static CSingleton instance;
//私有化构造器
private CSingleton() {}
//公有的静态方法,用于获取对象
public static CSingleton getInstance() {
//如果静态变量里没有地址,说明还未创建对象
if(instance == null) {
//创建对象,将地址存入静态变量
instance = new CSingleton();
}
//返回对象的地址,之前有就用之前的,没有就用新的
return instance;
}
}
在上述代码中,我们设置了一个私有的静态的属性 instance,然后私有化构造器,设置了一个公有的静态的getter方法,如果成员变量是null,说明还没有创建对象,那么就需要使用new关键字创建一个对象,如果对象的地址不为空,则返回该对象的地址
public static void main(String[] args) {
//获取两次该类型的对象
CSingleton c1 = CSingleton.getInstance();
CSingleton c2 = CSingleton.getInstance();
//使用 == 判断一下c1 c2 指向的是不是同一个对象
System.out.println(c1 == c2);
}
}
根据上述代码,我们设置了两个接收变量去接收两次调用getter方法返回值,判断两次的对象是不是一个地址最后的结果是一样的
二.继承
2.1继承的简介
继承是面向对象最显著的一个特征。继承是从已有的类中派生出新的类,新的类能吸收已有的类的属性和方法,并能扩展新的能力。
优点: 这种技术使得复用之前写过的代码非常容易,能大大缩短开发的周期,降低开发的费用
已有的类被称为父类或超类,派生出的新类被称为子类,也叫派生类。
在表示继承时需要使用extends关键字,语法如下
修饰词 class 子类名 extends 父类名{
子类的类体
}
2.2 继承的特点
1.Java只支持单继承,即一个父类可以有多个子类,但是一个子类只能有一个父类
2.Java支持多重继承,即一个子类在继承一个父类的同时,还可以被其他的类继承,继承具有传递性
3.子类会继承父类所有的成员变量和方法,包括私有的成员变量和方法,只是没有访问权限,也包括静态成员。
4.子类不能继承父类中的构造方法,只能调用父类中的构造方法,子类中至少应该调用一个父类的构造器。
5.子类在拥有父类的成员变量的基础上,还可以增加新的成员变量;
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;
}
//设置公共的setter和getter方法
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;
}
}
首先我们定义了一个父类APerson,并设置成员变量分别是name,age, gender,在父类中提供一个无参构造器,一个全参构造器。并设置公共的静态的getter/setter方法。
之后我们再定义一个student子类,这个子类既具有父类APerson人的特征,又具有学生的特征;
可以设置两个成员变量,分别是学号和班号
我们主要研究一下继承过程中构造器相关的问题:
1.子类不能继承父类的构造器
2.子类的构造器总,可以使用super(有参传参)的形式调用父类中的构造器
3.super(有参传参)和this(有参传参)一样,必须要放在构造器的首行首句
4.子类的构造器中至少有一个调用了父类的构造器
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 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);
//第四个和第五个参数是本类的,可以直接使用this.
}
我们先定义了子类的成员变量 studentId和classNo,然后提供一个子类的构造器,在该构造器中首行的super(有参传参)就是在调用父类中的全参构造器,之后我们重载了一个构造器,其中有五个参数,三个是从父类中继承过来的,还有两个是本类中的,
如果我们直接使用this.name是无法进行访问的,因为在父类中,成员变量是私有化的,虽然子类继承了,但是却无法访问。我们可以使用父类提供的getter/setter方法来进行访问和设置成员变量,在构造器的首行,我们使用了this(有参传参)是在调用本类的两个参数构造器,也就是上面的,然后调用setName 方法,因为方法是公有的可以直接使用
public String getStudentId() {
return studentId;
}
public void setStudentId(String studentId) {
this.studentId = studentId;
}
public String getClassNo() {
return classNo;
}
public void setClassNo(String classNo) {
this.classNo = classNo;
}
//添加toString方法,显示对象的属性值
public String toString() {
return "name:"+getName()
+",age:"+getAge()
+",gender:"+getGender()
+",studentId:"+studentId
+",classNo:"+classNo;
}
}
在子类中,又提供了公有的静态的getter和setter方法,然后添加了toString()方法,来显示对象中成员变量的具体值。
public class StudentTest {
public static void main(String[] args) {
//调用子类的5个参数构造器
Student xiaoming = new Student("小明",18,'男',"a56652","c1001");
// System.out.println(xiaoming);
String a = xiaoming.toString();
System.out.println(a);
//调用子类的2个参数的构造器
Student xiaohei = new Student("A200102","C1001");
System.out.println(xiaohei);
}
}
在测试环节就相对简单了不少,我们只需要调用子类的5个参数的构造器并对其一一赋值,然后查看结果就可以了对于查看结果,我们有两种方式,第一种是调用toString()方法,然后打印字符串类型的变量,第二种则是直接打印对象的名字;
2.3继承方法中的重写特点
重写,叫做override。在子类中,对于从父类中继承过来的方法进行重新的实现。在过程中,子类重写该方法,会自动覆盖掉从父类中继承过来的方法,因此,重写又被称为覆写。
重写的原因: 因为父类中的方法不能满足子类的需求了,因此子类需要改写逻辑
重写的特点:
1.子类只能重写父类中本身就存在的方法;
2.重写时,方法名和参数列表必须相同;
3.返回值类型必须与父类型相同,或者是其子类型
4.子类中重写的方法的访问权限必须大于或等于父类中的原方法的访问权限;
我们可以通过@Override这个注解来验证子类中的方法是不是重写,如果报错,则说明不是重写
重写和重载的区别:
重写是override,是子类对于父类中的方法的重新实现
重载是overload,是对于同一个类中的同方法名,不同参数列表的方法的描述
我们在接下来的实例中,设置了一个Animal父类,子类是TDog
class Animal {
private String color;
//公有的,返回值类型void
public void sport() {
System.out.println("---运动中---");
}
//公有的,返回值类型Animal
public Animal getMyClass() {
return null;
}
//默认的,返回值类型String
String showInfo() {
return null;
}
首先创建了一个父类Animal ,成员变量有color;公有的方法是sport(),还有一个公有的返回值类型是Animal类型的getMyclass()方法,还有一个返回值类型是String类型的showInfo(),需要注意的是,这个方法的权限修饰词是默认的;
public class TDog extends Animal{
public static void main(String[] args) {
//创建一个子类型对象
TDog dog = new TDog();
//调用继承过来的并且有访问权限的方法
dog.sport();
}
//子类dog独有的功能
//@Override 注解只能放在子类重写父类的方法上面,用于检测是不是重写。不是重写,报错
public void noise(){
System.out.println("---汪汪汪---");
}
//子类将父类的方法完全一模一样的写出来就是重写的一种
@Override
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;
}
}
代码量相对较多,我们需要逐步理解,首先使用extends关键字确定继承关系,定义的第一个方法noise(),这个方法并不是重写,因为父类中没有这个方法,如果加上注解@Override的话就会报错;这个方法是子类TDog独有的方法,
下一个方法sport( )属于重写,因为这个方法在父类中出现过,
下一个方法getMyClass()也属于重写,因为方法的目的是返回类型,所以我们把返回值类型更改为子类名,因为子类在重写方法时返回值可以与父类中的方法相同,也可以是其子类型,TDog就是Animal的子类型,
下一个方法showInfo()也属于重写,但是需要注意的是,子类在重写父类中的方法时,需要让访问权限修饰词的权限大于等于父类的访问权限修饰词,也可以理解为(青出于蓝胜于蓝)所以我们使用public来修饰;
最后一个方法showInfo()这个方法是子类独有的方法,因为形参列表与父类中的不同,这个方法与上述复写的方法属于重载关系;
在创建了子类对象后就可以使用对象名.方法名来调用有权限的方法了
dog.sport(); dog.noise(); dog.getMyClass();
getMyClass()方法的返回值是null,所以没有打印出来
2.4 Object类型
Object类型,是所有类型的顶级父类,可以这么说,我们定义的所有类,其父类或父类的父类都是Object类,因此在Object类中定义的属性、方法,在所有的类中都有包含。 比如常用的方法 hashCode(),equals(),toString(),getClass(),wait(),notify(),notifyAll()等
由于很多方法都是继承过来的,我们需要进行重写来满足子类的需求;
2.4.1 toString( )方法
toString( )的作用,用于将变量里的属性信息拼凑起来,返回成字符串的类型。这样的话我们就可以查看到每个对象的样子了;但是Object里提供的该方法逻辑,返回的是类全名+@+hashCode值的16进制,对于我们来讲没有意义,因此在定义类型时,通常都需要重写该方法;如果不重写的情况下,输出语句中会自动调用,所以返回的结果会是一串十六进制的数字
我们首先定义一个Teacher类,设置属性是名字,年龄,性别,并提供无参构造器和全参构造器
public class Teacher {
private String name;
private int age;
public char gender;
public Teacher(){}
public Teacher(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
然后我们重写一下toString()方法并使用main方法查看一下;
@Override
public String toString() {
return "Teacher{" + "age=" + age + ", gender=" + gender + ", name='" + name + '\'' + '}';
}
public static void main(String[] args) {
Teacher teacher = new Teacher();
System.out.println(teacher);
通过上述代码和返回结果,我们可以发现在重写方法后,返回的结果不再是16进制数字了,而变成了年龄和性别,名字的默认值;
//创建两个老师
Teacher t1 = new Teacher("Micheal", 39, '男');
Teacher t2 = new Teacher("Micheal", 39, '男');
System.out.println(t1==t2);//比较地址,返回false
boolean r = t1.equals(t2);//比较两个对象的属性是否相同
System.out.println("r:"+r);//返回true
我们又创建了两个对象t1和t2,成员变量的赋值都是相同的,然后我们比较一下他们两个对象的地址,之后比较一下他们的属性值是否相同,在此之前我们还需要把equals()方法也进行重写
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;
}
这个方法的返回值是布尔类型,我们可以根据形参输入的内容来做判断语句,如果obj是空值,则返回false,如果obj是不是要比较的对象,如果是就返回true,判断传入的obj和要比较的对象是不是同一类型,如果不是,就要转成同类型再去比较。其余的情况返回值都是false
通过运行结果和上述代码可以发现,虽然t1和t2的属性相同,但是他们是两个对象,因此地址值不相同,但是由于赋值相同,所以equals返回的值是true
2.5 static修饰词
static修饰的内容,是公有的,都是属于类的,因此应该直接使用类名调用,不属于对象
修饰方法:
静态方法是属于类的,不能使用引用类型变量调用,不合理,因为不属于对象,应该使用类名来调用
静态方法不能直接访问非静态成员
static方法不能被重写,但是子类可以提供和父类相同的静态方法(各是各的)
修饰代码块:
可以修饰静态代码块,在类加载期间执行,可以加载静态资源如图片,音频,视频等
修饰类:可以修饰内部类
public class MyUtil {
private String name;
//定义一个水桶的容量,单位是升
public static int contain = 18;
//如果想要达到每时每刻看到的共有资源都是一样的,那么就应该使用final修饰,即常量
public static final double PI = 3.14;
首先我们定义了一个MyUtil类,在里面定义了一个成员变量是水桶的容量,单位是升,初始化值是18;如果想让每时每刻看到的公共资源是一样的,就需要使用final修饰词,不能被改变,像下面的PI一样
public static void sum(int a, int b) {
//不能直接访问非静态成员
// this.name = "abc"
//addContain();
}
public void addContain() {
this.contain++;
}
public void subContain() {
this.contain--;
}
public static void main(String[] args) {
MyUtil P1 = new MyUtil();
//查看容量,使用变量调用,但是不建议
System.out.println(MyUtil.contain);
P1.addContain();
MyUtil P2 = new MyUtil();
//注意:静态变量,要使用类名.调用
System.out.println(MyUtil.contain);
我们定义了一个静态的方法,是不可以在方法内部访问非静态的成员变量的,又设计了一个加水一个减水的方法,是可以通过对象名进行调用的,在main方法里,我们先实例化了两个对象P1和P2
在查看水的容量时,由于容量是静态的公有的资源,所以不要使用对象名调用,因为共有资源不属于对象,需要使用类名.静态成员变量来调用;
class sub extends MyUtil {
// @Override 添加注解后报错,因为static方法不能被重写,但是子类可以提供和父类一样的静态方法(各是各的)
public static void sum(int a, int b) {
//不能直接访问非静态成员
// this.name = "abc"
//addContain();
}
}
在上述代码中,我们设计了sub作为MyUtil的子类,在子类中是无法重写静态的方法的,只能在子类中提供一个和静态方法相同的方法,但是这并不属于重写。
2.6 final修饰词
final修饰词表示是最后的,不能再更改的
修饰类: 表示该类不能再有子类了,即该类不能被继承
修饰成员变量: 只能初始化一次,不能再次赋值(常量)
修饰方法: 表示该方法不能被重写了
修饰局部变量
public final int sum(int a, int b) {
//测试final修饰局部变量
final int x;
x = 100;
// x = 101; 该变量不能再次赋值
return a + b;
}
我们可以通过上述的方法来测试final修饰局部变量,在方法体中如果使用final修饰完x变量之后,再给x赋值就会不成功
public class Cat {
private final String name;
public Cat(String name) {
this.name = name;
}
//研究一下父类里的方法被final修饰,是否还能重写
class BCat extends Cat {
public BCat() {
super("B");
}
//不能重写父类里的final方法,否则报错
// public int sum(int a, int b) {
// return a + b;
// }
}
再子类中测试一下能否重写final修饰的方法;很明显是不可以重写的,否则就会报错,只能改变形参列表然后重载
三. 多态
第一种类型:向上转型(向上造型)
父类型的变量引用子类型的对象
public class Animal {
private String color;
private String name;
private int age;
public Animal(){}
public Animal(String color, String name, int age) {
this.color = color;
this.name = name;
this.age = age;
}
//动物都会有发出声音的行为
public void noise(){
System.out.println("---动物都会发出叫声");
}
}
class Dog extends Animal{
public Dog(String color, String name, int age){
super(color, name, age);
}
public void noise(){
System.out.println("--汪汪汪--");
}
public void LookHouse(){
System.out.println("---会看家---");
}
}
class Cat extends Animal{
public Cat(String color, String name, int age){
super(color, name, age);
}
public void noise(){
System.out.println("--喵喵喵--");
}
public void getMouse(){
System.out.println("---会抓老鼠---");
}
}
我们先定义了一个父类Animal,两个子类Dog和Cat,其中,父类的成员变量有颜色,姓名,年龄,都是私有化的,提供了一个全参构造器,一个无参构造器,并提供了一个公有方法noise()
两个子类中也有独特的方法,并都提供了全参构造器
public class AnimalTest {
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();
}
}
这是一个测试类,定义了一个test1方法,形参列表是Animal类型的变量;内部可以调用该变量的noise()方法;
在main方法中,使用Animal类型去声明一个变量,引用Cat对象,之后调用noise()方法,由于这个方法在父类中也有,在执行期间,执行的是对象的类型的方法逻辑,所以会返回喵喵喵
但是抓耗子的方法就不能调用了,因为父类中没有该方法,编译期间看的是变量类型,会报错;
后面调用test(a),可以向上造型,方法内部调用的是Cat类型的方法逻辑,所以会返回喵喵喵;
而新实例化了一个Dog类的dog对象,也可以调用test1方法,此时方法内部调用的就是Dog类型的方法逻辑,就会返回汪汪汪
第二种类型:向下造型
父类型的变量需要赋值给子类型的变量,需要强制转换;
该操作可能会失败,失败的话就报异常 ClassCastException 类造型异常
为了避免失败,我们可以使用一个instanceof关键字来判断:该变量的指向对象是否属于某一个类型,如果是就返回true,如果不是就返回false;
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 cat = (Cat)am;
// cat.getMouse();
//我们应该避免上述情况发生,只需要使用instanceof即可
if(am instanceof Cat){ //因为am指向的是一个Dog对象不属于Cat类型,因此进不去分支
//所以避免了报错
Cat c = (Cat)am;
c.getMouse();
}
}
}
根据上述代码,我们可以发现我们先使用animal类型引用了子类型的变量,这个时候如果调用noise()方法会返回汪汪汪,但是调用看家的方法却不可以,因为编译期间父类型中没有看家的方法;我们需要向下转型才能调用该方法;使用Dog d = (Dog)am ; 这样就可以使用d来调用看家的方法了,在真正编程的期间可能会写错,导致强制转换的类型不正确,需要使用instanceof关键字
因为am指向的是一个Dog对象而不属于Cat类型,因此进不去分支,避免了报错。