面向对象(高级)
类变量(静态变量)和类方法(静态方法)
class Child{
//定义一个静态变量count
//该变量最大的特点就是会被Child类所有的对象实例共享
//类变量,可以通过类名来访问
public static int count = 0;
...
...
}
类变量内存布局
jdk-8:
类变量细节
静态变量的访问修饰符的访问权限和范围和普通属性是一样的
类变量是随着类的加载而创建, 所以即使没有创建对象实例也可以访问
什么时候需要用类变量
当我们需要让某个类的所有对象都共享一个变量时, 就可以考虑使用类变量(静态变量) : 比如 : 定义学生类, 统计所有学生共交多少钱
类变量与实例变量(普通属性)区别
类变量是该类的所有对象共享的, 而实例变量是每个对象独享的
类变量的生命周期是随着类的加载开始, 随着类消亡而销毁
类方法细节
静态方法可以访问静态属性
什么时候需要类方法
当方法中不涉及到任何和对象相关的成员, 则可以将方法设计成静态方法, 提高开发效率
比如: 工具类中的方法utils Math类\Arrays类等等
在实际开发中, 往往会将一些通用的方法, 设计成静态方法, 这样我们不需要创建对象就可以使用, 比如打印一维数组, 冒泡排序, 完成某个计算任务等等
类方法和普通方法都是随着类的加载而加载, 将结构信息存储在方法区: 类方法中无this的参数
普通方法和对象有关, 需要通过对象名调用, 类方法可以通过类名调用, 也可以通过对象名调用
类方法中不允许使用和对象有关的关键字, 比如this和super. 普通方法可以
类方法中只能访问静态变量或静态方法
普通成员方法, 既可以访问普通变量(方法), 也可以访问静态变量(方法) (必须遵守访问权限)
深入理解main方法
main方法是虚拟机调用
java虚拟机需要调用类的main()方法, 所以该方法的访问权限必须是public
java虚拟机在执行main()方法时不必创建对象, 所以该方法必须是static
该方法接收String类型的数组参数, 该数组中保存执行java命令时传递给所运行的类的参数
//测试 public class Hello{ public static void main(String[] args){ //args 是如何传入 //遍历显示 for(int i = 0;i < args.length;i++){ System.out.println("第" + (i + 1) + "个参数=" + args[i]) } } }
java 执行的程序 参数1 参数2 参数3
在main()方法中, 我们可以直接调用main方法所在类的静态方法或静态属性
但是不能直接访问该类中的非静态成员, 必须创建该类的一个实例对象后, 才能通过这个对象去访问类中的非静态成员
public class Main01{ //静态变量 private static String name = "jack"; //非静态变量 private int n1 = 100; public static void main(String[] args){ //可以直接使用name //1.静态方法可以访问本类的静态成员 System.out.println("name=" + name); //2.静态方法不可以访问本类的非静态成员 System.out.println("n1=" + n1); //错误 Main01 main01 = new Main01();//先创建一个本类的对象, 再调用n1 System.out.println("n1=" + main01.n1); } }
代码块使用细节
static代码块也叫静态代码块, 作用就是对类进行初始化, 而且它随着类的加载而执行, 并且只会执行一次(因为类加载只会一次). 如果是普通代码块, 每创建一次对象, 就执行
类什么时候被加载
- 创建对象实例时(new)
- 创建子类对象实例时, 父类也会被加载
- 使用类的静态成员时(静态属性, 静态方法)
普通的代码块, 在创建对象实例时, 会被隐式的调用. 被创建一次, 就会被调用一次. 如果只是使用类的静态成员时, 普通代码块并不会执行(类加载与普通代码块没有关系)
创建一个对象时, 在一个类 调用顺序是 :
调用静态代码块和静态属性初始化(注意 : 静态代码块和静态属性初始调用的优先级一样, 如果有多个静态代码块和多个静态变量初始化, 则按他们定义的顺序调用)
调用普通代码块和普通属性的初始化(注意 : 普通代码块和普通属性初始化调用的优先级一样, 如果有多个普通代码块和多个普通属性初始化, 则按定义顺序调用)
调用构造器(优先级最低)
构造器的最前面其实隐含了 super() 和 调用普通代码块, 静态相关的代码块, 属性初始化, 在类加载时, 就执行完毕了, 因此是优先于构造器和普通代码块执行的
class A{ public A(){ //构造器 //这里有隐藏的执行要求 //1.super();//默认调用父类无参构造器 //2.调用普通代码块 } }
我们看一下创建一个子类时(继承关系), 他们的静态代码块, 静态属性初始化, 普通代码块, 普通属性初始化, 构造方法调用顺序如下 :
父类的静态代码块和静态属性(优先级一样, 按定义顺序执行)
子类的静态代码块和静态属性(优先级一样, 按定义顺序执行)
父类的普通代码块和普通属性初始化(优先级一样, 按定义顺序执行)
父类的构造器
子类的普通代码块和普通属性初始化(优先级一样, 按定义顺序执行)
子类的构造器
解读 :
(1)进行类的加载
1.1 先加载父类 1.2 再加载子类
(2)创建对象
2.1从子类的构造器开始 2.2进入父类的构造器 2.3调用父类的普通代码块 2.4执行父类的构造器 2.5返回子类执行普通代码块 2.6执行子类的构造器
静态代码块只能直接调用静态成员(静态属性和静态方法), 普通代码块可以调用任意成员
单例设计模式
饿汉式
- 构造器私有化 - 防止直接 new
- 类的内部创建对象(该对象是static)
- 向外暴露一个静态的公共方法, 返回对象
- 代码实现
懒汉式
- 构造器私有化 - 防止直接 new
- 定义一个静态属性, 不直接new
- 向外暴露一个静态的公共方法, 返回对象
- 在静态公共方法里面进行判断此时是否创建对象, 如果没有创建对象, 那么就创建一个对象
饿汉式与懒汉式的区别
- 二者最主要的区别在于创建对象的时机不同 : 饿汉式是在类加载就创建了对象实例, 而懒汉式是在使用时才创建
- 饿汉式不存在线程安全问题, 懒汉式存在线程安全问题
- 饿汉式存在浪费资源的可能. 因为如果程序员一个对象都没有使用, 那么饿汉式创建的对象就浪费了, 懒汉式是使用时才创建, 就不存在这个问题.
final关键字
使用场景
- 当不希望类被继承时
- 当不希望父类的某个方法被子类覆盖/重写
- 当不希望类的某个属性的值被修改(常量)
- 当不希望某个局部变量被修改(局部常量)
final细节
final修饰的属性又叫常量, 一般用XX_XX_XX来命名
final修饰的属性在定义时, 必须赋初值, 并且以后不能再修改, 赋值可以在如下位置之一选择一个位置赋初值 :
定义时 :
public final double TAX_RATE = 0.08;
在构造器中
在代码块中
class AA{ /* 1.定义时:如 public final double TAX_RATE=0.08; 2.在构造器中 3.在代码块中 */ public final double TAX_RATE = 0.08; public final double TAX_RATE2; public final double TAX_RATE3; public AA(){ TAX_RATE2 = 1.1; } { TAX_RATE3 = 0.8; } }
如果final修饰的属性是静态的, 则初始化的位置只能是 :
- 定义时
- 静态代码块 (不能在构造器中赋值)
final类不能继承, 但是可以实例化对象
如果类不是final类, 但是含有final方法, 则该方法虽然不能重写, 但是可以被继承
一般来说, 如果一个类已经是final类了, 就没有必要再将方法修饰成final方法
final不能修饰构造器
包装类, String都是final类
final 和 static 往往搭配使用, 效率更高, 不会导致类加载, 底层编译器做了优化处理
public class Something{
public int addOne(final int x){//final可以修饰形参
++x;//错误,不能修改final x 的值
return x + 1;//可以,没有改变x的值
}
}
抽象类
当父类的某些方法, 需要声明, 但是又不确定如何实现时, 可以将其声明为抽象方法, 那么这个类就是抽象类
抽象类细节
- 抽象类不能被实例化
- 抽象类不一定要包含abstract方法. 也就是说, 抽象类可以没有abstract方法
- 一旦类包含了abstract方法, 则这个类必须声明为abstract
- abstract只能修饰类和方法, 不能修饰属性和其他的
- 抽象类可以有任意成员(因为抽象类还是类), 比如 : 非抽象方法, 构造器, 静态属性等等
- 抽象方法不能有主体, 即不能实现
- 如果一个类继承了抽象类, 则它必须实现抽象类的所有抽象方法, 除非它自己也声明为abstract类
- 抽象方法不能使用private, final, static来修饰, 因为这些关键字都是和重写相违背的
模板设计模式-抽象类最佳实践
抽象类体现的就是一种模板模式的设计, 抽象类作为多个子类的通用模板, 子类在抽象类的基础上进行扩展, 但子类总体上会保留抽象类的行为方式
- 当功能内部一部分实现是确定的, 一部分实现是不确定的. 这时可以把不确定的部分暴露出去, 让子类去实现
- 编写一个抽象父类, 父类提供了多个子类的通用方法, 并把一个或多个方法留给其子类实现, 就是一种模板模式
接口
接口细节
接口不能实例化
接口中所有的方法是public方法, 接口中的抽象方法, 可以不用abstract修饰
一个普通类实现接口, 就必须将该接口的所有方法都实现
抽象类实现接口, 可以不用实现接口的方法
abstract class Cat implements IA{ //不会报错 }
一个类同时可以实现多个接口
接口中的属性, 只能是final的, 而且是 public static final 修饰符
interface IA{ int n1 = 10;//等价 public static final int n1 = 10; }
接口中属性的访问形式 : 接口名.属性名
接口不能继承其他类, 但是可以继承多个别的接口
interface B { } interface C { } interface A extends B,C{ }
接口的修饰符只能是public 和默认, 这点和类的修饰符是一样的
接口与继承的区别
接口实现机制是对单继承机制的补充.
- 当子类继承了父类, 就自动的拥有父类的功能
- 如果子类需要扩展功能, 可以通过实现接口的方式扩展
接口和继承解决的问题不同
- 继承的价值主要在于 : 解决代码的复用性和可维护性
- 接口的价值主要在于 : 设计, 设计好各种规范(方法), 让其他类去实现这些方法
接口比继承更加灵活
- 继承是满足 is - a 的关系, 而接口只需满足 like - a 的关系
接口多态特性
多态参数
多态数组
//main中 Usb[] usbs = new Usb[2]; usbs[0] = new Phone_(); usbs[0] = new Camera_(); /* 给Usb数组中, 存放Phone 和 Camera 对象, Phone类还有一个特有的方法call(), 请遍历Usb数组, 如果是Phone对象, 除了调用Usb 接口定义的方法外, 还需要调用Phone特有方法Call() */ for( int i = 0; i < usbs.length; i++){ usb[i].work();//动态绑定.. //进行类型判断, 然后进行类型的向下转型 if(usb[i] instanceof Phone_ ){ ((Phone_)usbs[i]).call(); } } //接口 interface Usb{ void work(); } class Phone_ implements Usb{ public void call(){ System.out.println("手机可以打电话"); } @Override public void work() { System.out.println("手机工作中..."); } } class Camera_ implements Usb{ @Override public void work() { System.out.println("相机工作中..."); } }
接口的多态传递现象
内部类
类的五大成员: 属性, 方法, 构造器, 代码块, 内部类
内部类的分类
定义在外部类局部位置上 (比如方法内)
- 局部内部类 (有类名)
- 匿名内部类 (没有类名)
定义在外部类的成员位置(属性或者方法)上
- 成员内部类 (没用static修饰)
- 静态内部类 (使用static修饰)
局部内部类
class Outer01{//外部类
private int n1 = 100;
public void m1(){//方法
//1.局部内部类是定义在外部类的局部位置, 通常在方法
//3.不能添加访问修饰符, 因为它的地位就是一个局部变量. 局部变量不能使用修饰符, 但是可以使用final修饰, 因为局部变量也可以使用final
//4.作用域 : 仅仅在定义他的方法或代码块中
class Inner01{//局部内部类
private int n1 = 800;//成员重名
//2.可以直接访问外部类的所有成员, 包含私有的
public void f1(){
System.out.println("n1=" + n1);
//解读: Outer01.this 本质就是外部类的对象, 即哪个对象调用了m1, Outer01.this就是哪个对象
System.out.println("外部类的n1=" + Outer01.this.n1);
}
}
//5.外部类在方法中, 可以创建内部类的对象,然后调用方法(注意: 必须在作用域内)
new.Inner01().f1();
}
}
//6.外部其他类, 不能访问局部内部类
//7.如果外部类和局部内部类的成员重名时, 默认遵循就近原则, 如果想访问外部类的成员, 则可以使用(外部类名.this.成员)去访问
匿名内部类
匿名内部类是定义在外部类的局部位置, 比如方法中, 并且没有类名
- 本质是类
- 是一个内部类
- 该类没有名字
- 同时还是一个对象
//基于接口的匿名内部类
//需求: 想使用IA接口, 并创建对象
//传统方式: 是写一个类(Dog类), 实现该接口, 并创建对象
//但是如果我们 Dog只是使用一次, 后面不再使用
//
//使用匿名内部类
//此时 Dog的编译类型是 IA, 运行类型是匿名内部类
IA Dog = new IA(){
@Override
public void cry(){
System.out.println("狗狗叫唤...");
}
};//注意分号
//基于类的匿名内部类
//father 编译类型 Father, 运行类型是匿名内部类
//注意("jack")参数列表会传递给 构造器
Father father = new Fatehr("jack"){
@Override
public void test(){
System.out.println("匿名内部类重写了test方法");
}
};
father.test();
//如果此时father是一个抽象类
//那么必须要是实现抽象类中的抽象方法
匿名内部类的细节
两种调用方法, 匿名内部类既是一个类的定义, 同时它本身也是一个对象
可以直接访问外部类的所有成员, 包含私有的
不能添加访问修饰符, 因为它的地位就是一个局部变量
作用域 : 仅仅在定义它的方法或代码块中
外部其他类不能访问匿名内部类
如果外部类和匿名内部类的成员重名时, 默认遵循就近原则, 如果想访问外部类的成员, 则可以使用(外部类名.this.成员)去访问
匿名内部类实践-当作实参直接传递
//main中
public static void f1(IL il){
il.show();
}
//当作实参直接传递
f1(new IL(){
@Override
public void show(){
System.out.println("123...");
}
});
}
interface IL{
void show();
}
成员内部类
class Outer01{//外部类
private int n1 = 10;
public String name = "jack";
//成员内部类, 是定义在外部内的成员位置上
class Inner01{//成员内部类
public void say(){
//可以直接访问外部类的所有成员, 包含私有的
System.out.println("n1 =" + n1 + "name =" + name);
}
}
//写方法
public void t1(){
//使用成员内部类
Inner01 inner01 = new Inner01();
inner01.say();
}
}
//1.可以添加任意访问修饰符(public, protected, 默认, private) 因为它的地位就是一个成员
//2.成员内部类 - 访问 - 外部类成员
// 访问方式 : 直接访问
//3.外部类 - 访问 - 成员内部类
// 访问方式 : 创建对象,再访问
//4.如果外部类和内部类的成员重名时, 默认遵循就近原则, 如果想访问外部类的成员, 则可以使用(外部类名.this.成员)去访问
外部其他类 - 访问 - 成员内部类 //看别人博客
静态内部类
使用static修饰, 定义在外部类的成员位置