1.封装(Encapsulation)
(1)封装的简介
从广义的角度来说:将一块经常要使用的代码片段,定义到方法中,是封装。将多个方法和多个状态数据定义到类体中,也是一种封装。定义方法是封装,定义类也是封装
从狭义的角度来说,java的封装,就是把类的属性私有化(private修饰),再通过公有方法(public)(getter/setter方法)进行访问和修改。
(2)属性的封装
1.为什么封装?
一个类中的某些属性,不希望直接暴露给外界,让外界直接操作。如果让外界直接操作的话,对这个属性进行的值的设置,可能不符合逻辑。此时就需要将这个属性封装起来,不让外界直接访问。
比如在一个类中有age(年龄)这个属性,让外界直接操作可能会被赋值成-10000,即不合逻辑。
class Person {
String name;
int age;
}
class Program {
public static void main(String[] args) {
// 实例化一个Person对象
Person xiaoming = new Person();
xiaoming.name = "xiaoming";
xiaoming.age = -10000;
}
}
2.如何封装?
1.为了不让外界直接访问某些属性,用关键字private修饰这些属性。
2.提供公有的getter/setter的方法,用来操作被私有化的属性。
以后APerson类为例进行演示:
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 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;
}
}
可以通过公有的getter/setter方法来操作私有化的属性
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());
}
}
为什么私有化起来的属性,不希望外界访问,还需要再提供setter和getter?
答:可以通过指定的方法访问属性,在这些方法中,可以添加一些数据处理操作,让赋的值是我们需要的。
代码演示如下:
public void setAge(int age) {
if (age > 0 && age <= 120) {
this.age = age;
}else{
//如果赋值有问题,可以使用下面的方式让程序中断,并进行提示
throw new RuntimeException("--您赋值的年龄不合法--");
}
}
public void setGender(char gender) {
if(gender == '男' || gender == '女'){
this.gender = gender;
}else{
//如果赋值有问题,可以使用下面的方式让程序中断,并进行提示
throw new RuntimeException("您赋值的性别不合法,需要汉字'男'或'女'--");
}
}
2.单例设计模式
(1)简介
单例模式 :是一种常用的软件设计模式,属于创建型模式之一。它的目的是确保一个类只有一个实例,并提供一个全局访问点。
使用场景:
-
频繁创建和销毁的对象:如果对象创建和销毁的成本较高,且在程序运行期间需要频繁访问,使用单例模式可以提高效率。
-
控制资源访问:例如,数据库连接、日志对象、配置管理器等,这些资源通常希望在整个应用中只有一份实例。
-
工具类:对于一些工具类,如缓存、对话框、注册表设置等,使用单例模式可以简化代码,避免重复实例化。
(2)单例的饿汉模式
1. 提供一个该类的,私有的,静态的属性, 并在静态代码块里赋值
2. 提供一个私有的构造器, 避免外界直接调用构造器
3. 提供一个公有的,静态的方法,来获取当前类的对象
以BSingleton类进行演示:
public class BSingleton {
//提供一个私有的,静态的,该类的属性instance
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,即创建的两个引用变量的地址是一样的,指向的是同一个对象。
(3)单例懒汉模式
1. 提供一个该类的,私有的,静态的属性。
2. 提供一个私有的构造器, 避免外界直接调用构造器
3. 提供一个公有的,静态的方法,来创建当前类的对象。如果对象不存在,说明对象还未创建。如果对象存在,则返回已存在的对象
4. 懒汉模式,有线程安全隐患问题,比如两次调用的时候都刚好执行到 判断等于null,都会执行到 创建实例那一行代码。那么就会在内存中创建出来两个对象。
以CSingleton类进行演示:
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,即c1和c2的地址是一样的,指向的是同一个对象。
总结:单例饿汉模式和单例懒汉模式基本都一样的,都可以获取到一个类的唯一的对象。
1 、在没有使用获取当前类对象之前,懒汉式单例比饿汉式单例在内存上有较少的资源占用。
2 、懒汉式单例在多线程的环境下有问题。需要考虑线程安全(可以在公有的静态方法上加锁synchroniaed)
3.继承
(1)简介
是面向对象最显著的一个特征。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。 这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。
已有的类,叫==父类==,又叫基类,超类。 派生出来的新类,叫==子类==,也叫派生类。
使用关键字extends 来表示子类继承了父类,语法如下:
修饰词 class 子类名 extends 父类名{
//子类的类体
}
比如:
class A{} //父类
class B extends A{} //B是A的子类
class C extends B{} //C是B的子类
(2)继承特点
1.Java只支持单继承,即一个类只能有一个父类;但是一个类可以有多个子类。
public class classA{}
public class classB exends classA{}
不同类继承同一个类
public class classA{}
public class classB extends classA{}
public class classC extends classA{}
2 、java支持多重继承,即一个类在继承自一个父类的同时,还可以被其他类继承。 可见继承具有传递性
public class classA{}
public class classB extends classA{}
public class classC extends classB{}
3、子类继承了父类的所有成员变量和方法,包括私有的(只不过没有访问权限),当然也包括静态成员。
public class APerson {
private static String name;
private int age;
private char gender;
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;
}
public static void sum(int a,int b){
System.out.println(a+b);
}
public void sub(int a,int b){
System.out.println(a-b);
}
}
class Student1 extends APerson{
private String studentId;
private String classNo;
public static void main(String[] args) {
Student1 s = new Student1();
//父类的成员是私有的,子类都没有访问权限
//s.name="xiaoming";
//s.age = 19;
//可以通过父类提供的公有的方法访问和设置属性
s.setName("小张");
s.setAge(19);
s.setGender('男');
//获取父类的属性
System.out.println(s.getName());
System.out.println(s.getAge());
System.out.println(s.getGender());
//调用父类的静态方法
s.sum(4,5);
//调用用父类的普通方法
s.sub(8,3);
}
}
4、子类不会继承父类的构造方法,只能调用父类里的构造方法,并且一定至少有一个子类构造器调用了父类的构造器。
以上述代码为例:子类Student1的无参构造
public Student1(){
super();
}
系统给父类提供了一个无参构造器,子类继承时要调用这个无参构造器,但是如果父类的构造器没有重写,那么子类构造器里的super()是隐藏的,可以省略不写。即一定至少有一个子类构造器调用了父类的构造器。
为什么要至少有一个调用了父类的构造器: 因为子类继承过来的属性需要初始化。
5 、子类在拥有父类的成员的基础上,还可以添加新成员。如3.的代码,子类Student1里有tudentId和classNo两个自己的属性。
(3)继承中的构造器
一个对象在实例化的时候,需要在堆上开辟空间,堆中的空间分为两部分,分别是从父类继承到的属性,子类特有的属性。而实例化父类部分的时候,需要调用父类中的构造方法(默认调用的是父类中的无参构造器 )。如果父类中没有无参构造器,那么子类需要显式调用父类中的某一个有参构造器。
在子类的构造方法中,使用==super(有参传参)==调用父类中存在的构造方法,而且==super(有参传参)==必须放在首行首句的位置上。因此可知,super(有参传参)和this(有参传参)不能在一个构造器中共存。
子类中构造器的调用的代码演示:
public class APerson {
private String name;
private int age;
private char gender;
//父类中提供了其他构造器,运行时系统不再提供无参构造了,则子类调用时需要显示的调用父类的构造器
public APerson(String name) {
this.name = name;
}
public APerson(String name, int age) {
this.name = name;
this.age = age;
}
public APerson(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
class Student1 extends APerson{
private String studentId;
private String classNo;
public Student1(){
//如果子类构造器中没有参数,显示调用父类的构造器时,使用super(给具体的值)调用父类的构造器
//super("小张");//调用父类中一个参数的构造器
//super("小明",19,'男');//调用父类中三个参数的构造器
super("小红",15);//调用父类中两个参数的构造器
//如果子类的构造器没有参数,显示调用父类构造器时直接赋值。
}
public Student1(String name, String studentId){
//如果子类构造器中有父类的属性形参,则使用super(有参传参)显示调用父类的构造器
//super(name);
super(name,16);
this.studentId = studentId;
}
public Student1(String name,String studentId,String classNo){
super(name,18,'男');
this.studentId = studentId;
this.classNo = classNo;
}
public Student1(String name ,int age ,char gender){
super(name,age,gender);
}
public Student1(String name ,int age ,char gender,String studentId,String classNo ){
this(name,age,gender);//调用本类的构造器
this.studentId = studentId;
this.classNo = classNo;
}
}
注意点:1.如果子类构造器中没有参数,显示调用父类的构造器时,使用super(给具体的值)调用父类的构造器
2.如果子类构造器中有父类的属性形参,则使用super(有参传参)显示调用父类的构造器
(4)继承中的方法重写
重写,叫做override。在子类中,对从父类继承到的方法进行重新的实现。 这个过程中,子类重写该方法,会覆盖掉继承自父类中的实现方法,因此,重写又叫做覆写。
之所以重写,是因为父类的方法逻辑不能满足子类的需求了,因此子类需要修改逻辑(重写)
重写的特点:
-- 子类只能重写父类中存在的方法。
-- 重写时,子类中的方法名和参数要与父类保持一致。(区别于重载overload)
如:子类将父类的方法完全一模一样的写出来,就是重写的一种。
-- 返回值类型:必须和父类方法的返回值类型相同,或者是其子类型。
-- 访问权限:子类重写方法的访问权限必须大于等于父类方法的访问权限。
private < 默认的 < protected < public
class Animal{
//公有的,返回值类型Animal
public Animal getMyClass(){
return null;
}
}
public class TDog extends Animal {
// 子类在重写父类的方法时,返回值类型与父类中的方法可以相同,也可以是其子类型 : TDog就是 Animal的子类型
@Override
public TDog getMyClass(){
return null;
}
}
@Override 注解只能放在子类重写父类的方法的上面,可以用于检测是不是重写。不是重写,报错。
误区:加了@Override就是重写,没有加@Override就不是重写。 这种说法是错误的! @Override只是进行的一个语法校验,与是不是重写无关。
简述 Override 和 Overload 的区别
Override: 是重写,是子类对父类的方法进行重新实现。
Overload: 是重载,是对同一个类中的同名、不同参数方法的描述
方法的重载:
1.方法名必须相同
2.参数列表必须不同
3.其他修饰符可以相同,也可以不同
4.可以抛出不同异常
方法的重写:
1.方法名、参数列表、返回值类型都必须相同
2.访问修饰符必须大于或等于被重写的方法
Java中四种权限修饰符的大小排列顺序 : private<default<producted<public
3.重写的方法中,不能抛出新的异常或比重写的方法更多、更大的异常,但一定会抛出异常。也就是说,只能抛出相同的异常或是被重写方法异常的子异常,还可以抛出非编译异常(RuntimeException)
4.重写方法只会存在于具有继承关系的子类中,而当父类中的方法用private修饰时,即使子类中有重名方法,也不叫方法的重写
5.非静态方法不能被重写成静态方法
(5)Object类型
1.toString()方法
该方法的作用是将对象的信息变成字符串。源码的返回值对我们意义不大,所以需要重写我们想要的输出逻辑。
该方法。在使用输出语句打印对象的变量时,会自动调用。
2.重写equals方法
重写要遵循一些原则:
1、如果 obj = null,一定要返回false。
2、如果 obj = this,一定要返回true。
3、如果两个对象的类型不同,一定要返回false。
4.如果 a.equals(b) 成立,则 b.equals(a) 也必须成立。
5、如果 a.equals(b), b.equals(c) 成立,则 a.equals(c) 也必须成立。
/**
* 重写equals方法
* 1. 判断一下,传入的是不是null
* 2. 判断一下,传入的是不是自己
* 3. 判断一下,是不是同类型,如果是,就转成同类型进行比较
* 4. 其他任何情况,都返回false;
*/
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;
}
(6)final修饰
final是java语法中的修饰词,可以用来修饰类,属性,方法,局部变量。 final是“最后,最终”的含义
--1.修饰类时,不能再有子类,即不能被继承, 比如String, 八大基本数据类型的包装类
public final class Cat {
}
//public class CatB extends Cat{ //会报错,Cat是final修饰的
//}
--2. 修饰属性:
被final修饰的变量:三种赋值方式
- 在定义时直接赋值。
- 声明时不赋值,在constructor中赋值(最常用的方式)
- 声明时不赋值,在构造代码块中赋值
- final修饰的变量必须赋值,且只能赋值一次。
三者中只要有一个出现,如果其他地方在为该变量赋值,则会报编译错误
public class Cat {
private final String name;
// private final String name = "小张";//直接初始化
public Cat(String name){ //在构造器里初始化
this.name = name;
}
只能存在一种赋值方式,两种同时存在时会出现编译错误
/**
* 下面的方法给name赋值,已经不是初始化了。因为想要执行该方法,那么
* 构造器一定先执行了
*/
// public void setName(String name){
// this.name = name; //name是final修饰的,只能被赋值一次,构造器先于该方法执行
// }
//还可以在构造代码块中赋值
// {
// name = "小红" ;
// }
}
--3. 修饰方法:
final修饰的方法,不能被重写, 但是能被重载。
public class Cat {
private final String name;
public final int sum(int a, int b){
//测试final修饰局部变量
final int x;
x = 100;
//x = 101; 该变量不能再次赋值。
return a + b;
}
}
//研究一下父类里的方法被final修饰,是否还能重写
class BCat extends Cat{
//不能重写父类里的final方法,如下代码会报错 是编译错误
//public int sum(int a,int b){
// return a + b;
//}
}
--4. 修饰局部变量, 只能赋值1次,不能第二次赋值。
再次赋值会报错,是编译错误。
public final int sum(int a, int b){
//测试final修饰局部变量
final int x;
x = 100;
// x = 101; // 该变量不能再次赋值。再次赋值会报错
return a + b;
}
(7)static修饰
1. 修饰成员变量:
静态成员变量,属于类的,公共资源,使用类名.调用。
注意:可以使用引用变量.调用,但是不合理,因为不属于对象。
2. 修饰方法:
- 静态方法,属于类的,公共资源,使用类名.调用。在本类main方法里直接方法名(有参传参)
- 注意:可以使用引用变量.调用,但是不合理,因为不属于对象。
- 静态方法中不能直接访问非静态成员。
public class MyUtil {
private String name;
public static int contain = 18;
public static void sum(int a,int b){
//不能直接访问非静态成员
//this.name = "aaaa";
//addContain();
contain = 19;
}
}
- static方法不能被重写 (即加上@override进行检测,报错,说明不是重写方法,子类可以提供和父类一样的静态方法,各是各的)
3. 修饰代码块:
- 静态代码块, 类加载时只执行一次。 通常用于加载静态资源:图片,音频等
4. 修饰类:
- 可以修饰内部类。
4.多态
(1)简介
多态:指的是一个对象具有多种形态的特点。就是一个对象可以从一种类型转换为另外一种类型。
下面是三个类:
public class Aminal {
private String color;
private String name;
private int age;
public Aminal(){}
public Aminal(String color, String name, int age) {
this.color = color;
this.name = name;
this.age = age;
}
//动物都会有发出声音的行为
public void noise(){
System.out.println("---动物都会发出叫声--");
}
}
class Dog extends Aminal {
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 Aminal {
public Cat(String color, String name, int age){
super(color, name, age);
}
public void noise(){
System.out.println("---喵喵喵--");
}
public void getMouse(){
System.out.println("---我会抓老鼠----");
}
}
(2)向上造型(向上转型)
-
父类型的变量引用子类型的对象。
父类型 变量 = new 子类型();
public class AminalTest {
public static void main(String[] args) {
//使用父类型 Aminal声明一个变量,引用一个子类Cat对象
Aminal a = new Cat("白色","小花",3);
}
-
向上转型肯定会成功,是一个隐式转换。
-
向上转型后的对象,将只能够访问父类中的成员(编译期间,看变量类型)
public class AminalTest {
public static void main(String[] args) {
//使用Animal声明一个变量,引用一个Cat对象
Aminal a = new Cat("白色","小花",3);
//调用方法:
a.noise(); // 编译期间不会出现问题。因为父类里有该方法。运行期间,执行的是对象的类型里的方法逻辑
//a.getMouse(); 调用不到该方法,因为a这个变量的类型里没有该方法, (编译期间,看变量类型)
}
-
如果调用的是重写过的方法,那么调用的一定是重写方法(运行期间,看对象)
-
应用场景:在定义方法时,形式参数是父类型的变量。这样更加灵活,可以传任意子类型的对象,这就是向上造型的优势所在。
public class AminalTest {
public static void main(String[] args) {
//使用Animal声明一个变量,引用一个Cat对象
Aminal a = new Cat("白色","小花",3);
//调用方法:
a.noise(); // 编译期间不会出现问题。因为父类里有该方法。运行期间,执行的是对象的类型里的方法逻辑
//a.getMouse(); 调用不到该方法,因为a这个变量的类型里没有该方法, (编译期间,看变量类型)
test1(a);
Dog dog = new Dog("黑色","大黑",8);
test1(dog);
}
//测试:执行动物的叫声 这就是向上造型的优势所在,
// 父类型的变量作为参数,更加灵活,可以传入不同的子类型对象
public static void test1(Aminal animal){
animal.noise();
}
}
(3)向下转型(向下造型)
-
父类型变量赋值给子类型的变量,需要强制转换,是一个显式转换。
public class AminalTest2 {
public static void main(String[] args) {
//创建一个父类型的变量引用子类型的对象
Aminal am = new Dog("黄色","大黄",5);
am.noise();
//调用一下对象的独有功能
//am.LookHouse(); //编译期间看变量类型。没有该方法,所以报错
//只能向下转型才可以,是强制转换,是一个显示的转换。
Dog d = (Dog)am;
d.LookHouse();
}
}
-
可能会失败,失败的话,会报类造型异常ClassCastException
-
为了避免ClassCastException ,可以使用instanceof 来判断:变量指向的对象是否属于某一个类型。
public class AminalTest2 {
public static void main(String[] args) {
//创建一个父类型的变量引用子类型的对象
Aminal 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();
}
}
}
-
为什么向下转型:前提,父类型的变量引用子类型的对象。但是父类型的变量调不了子类里独有的功能,已经不能满足需求了。