OOP
面向对象程序设计( Object Oriented Programming,OOP )
三大特性
- 封装
- 继承
- 多态
类
从编译器的角度看,嵌套包之间毫无关系,( 如:com.jumpig包 和 com.jumpig.joshua包 )
访问控制符:
- public
- 所有可见
- protected:只有内部类
- 本包和所有子类可见
- 默认
- 本包可见
- private:只有内部类
- 本类可见
修饰符:
- abstract
- final
- static:只有内部类
- 为什么顶级类不能用static,只有内部类可以。
- 我认为顶级类本身就是静态的,因为 Math.abs() 就可以用啊
- abs() 是静态方法,可以直接类名调用
- Math.abs() 这样用,也没有构造 Math 对象啊
// import static java.lang.Math.abs;
import static java.lang.Math.*; // 类的静态导入,允许导入静态方法和静态字段
public abstract class Person {
// 如果构造器中没有初始化,类中的字段(没有final修饰)会自动设置初值(数值为0,布尔值为false,对象为null)
private String name; // 属性,字段,实例字段,域,成员变量
private int age = abs(-123);
private int age = Math.abs(-123); // 若没有静态导入
public Person() { // 构造器
// 类只有通过构造器才能用,为了构造对象
// Math.abs()方法为什么能直接调用,因为该方法是 static 修饰
}
void Hello(int age) { // 方法
// 方法中的局部变量必须初始化
this.name = "张三"; //隐式参数
age = 18; // 显示参数
}
}
方法
访问控制符:
- public
- 所有可见
- protect
- 本包和所有子类可见
- 默认
- 本包可见
- private
- 本类可见
方法签名
方法签名:方法名、参数类型
两个方法的方法签名不能相同,其他都可以相同。
final
final字段必须在构造对象时初始化。
变量:
- 对象的引用不会改变。但是值可以改变(如:StringBuilder)
类:
- 该类不能被继承。
方法:
- 方法不能被重写。
【注释】:System类中有setOut方法可以修改final字段 public final static PrintStream out = null; 你会奇怪,因为这个是原生的方法,不是用Java实现的。
构造器
如果构造器的第一个语句形如 this(...) ,它将调用同一个类的另一个构造器
super(...) ,它将调用父类的构造器
字段
this. 将调用该类的字段
super. 将调用父类的字段(注意访问控制符)
protected
对于 protected 字段、方法 的访问规则一样,为了简便我们只拿字段作为例子。
public class Animal { // 父类
protected int i = 5;
}
public class Dog extends Animal {
public void show() {
Cat cat = new Cat(); // Cat 是不同包中的子类
// cat.i = 9; // 无法通过其他子类对象访问父类的protected成员
// 如果 Cat 在同一个包中,可行。
}
}
下面在不同的包中
public class Cat extends Animal {
public void eat() {
i = 7; // 可以直接访问
}
}
public class Cat extends Animal {
public void show() {
Cat cat = new Cat();
cat.i = 10; // 通过子类对象访问
Animal animal = new Cat();
// animal.i 不行
}
}
public class Cat extends Animal {
public void show() {
Animal animal = new Animal();
// animal.i = 9; // 无法通过父类对象访问
}
}
继承
class A {}
class B extends A {}
A : 超类、基类、父类
B : 子类、派生类、孩子类
子类的构造器如果没有显式调用父类构造器,会自动调用父类的无参构造器,如果父类没有无参构造器会报错。
子类字段允许和父类字段一样。
覆盖方法时,子类方法的:
可见性不能低于超类。如:超类为public,子类也必须为public。
返回类型不能低于超类。如:超类方法返回Employee对象,子类可以返回Employee或Manager类。
多态
-
多态是 OOP 技术中最为灵活的特性,它极大的提高了程序的可扩展性和代码的可维护性。
-
在Java语言中,对象变量是多态的,即对象变量既可以引用本类的对象,也可以引用子类的对象。
-
在运行时可以选择适当的方法(即:可能选择子类方法也可能选择父类方法),称为动态绑定。
public class Poly {
public static void main(String[] args) {
Employee e = new Manager("m1"); // e是一个父类
e.say(); // 自动调用了子类的方法,而不是父类的,称为动态绑定
// e.hello(); // Employee类中没有hello()方法
Manager[] managers = new Manager[10];
Employee[] employees = managers; // 这里managers[0]和employees[0]是相同的引用
employees[0] = new Employee(""); // 这里编译器竟然接受了这个操作,但运行会出错
}
}
class Employee {
private final String name;
public Employee(String name) { this.name = name; }
void say() { System.out.println(name); }
public String getName() { return name; }
}
class Manager extends Employee {
public Manager(String name) { super(name); }
@Override
void say() { System.out.println("--" + getName()); }
void hello() { System.out.println("Manager特有"); }
}
Employee e; //员工
Manager m; //经理
// 经理 继承 员工
e = m; //向上转型是自动进行的
m = e; //编译报错
m = (Manager)e; //强制向下转型
// 强制向下转型可能会失败,强制向下转型前用instanceof判断
if (e instanceof Manager) {
m = (Manager)e;
}
多态规则
- 如果父类和子类有相同的方法,会调用子类方法。什么叫做相同的方法呢?
- 方法名:方法名要一样
- 参数类型:参数要完全一样,参数类型就算存在子类父类的关系也不行
- 返回类型:子类方法的返回类型 是 父类方法的返回类型的 子类,或返回类型相同
抽象类
- 抽象类可以不包含抽象方法
- 包含抽象方法的类必须是抽象类
- 抽象类不能实例化,但是允许有构造器,匿名内部类可以使用这个构造器。
- 最多只能继承一个类(可以是抽象类或普通类)
- 可以不覆盖接口中的抽象方法
- 抽象方法就是为了重写,所以不能是 private 修饰。
接口
- 接口中的方法默认 public abstrat ,字段 public static final 。Java 9 可以使用 private 方法。
- 实现接口时必须把方法声明为 public ,否则指出你试图提供更严格的访问权限。
- Java 8 可以让接口为 default,可以不重写 default 的方法。
- Java 8 允许方法用 static 修饰。
- 一个接口扩展(extends)多个接口。可以覆盖其他接口中的方法。
- 接口没有构造器,不能用 new 运算符实例化一个接口。
- 但是接口也是继承 Object 类,可以重写 Object 方法。(这是有点奇怪哦)
- 接口必须引用实现了这个接口的类对象。
- 可以用 instanceof 检查一个对象是否实现了某个接口。
- 任何实现了接口的类都自动地继承了接口中的常量,可以直接使用。
Java 8 可以让接口为 default,可以不重写 default 的方法。
Java 8 允许给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可,这个特征又叫做扩展方法(也称为默认方法或虚拟扩展方法或防护方法)。在实现该接口时,该默认扩展方法在子类上可以直接使用,它的使用方式类似于抽象类中非抽象成员方法。
Note:扩展方法(接口中的default方法)不能够重写 Object 中的方法,却可以重载 Object 中的方法。
eg:toString、equals、hashCode 不能在接口中被覆盖,却可以被重载。
Note:
JVM平台的接口的默认方法实现是很高效的,并且方法调用的字节码指令支持默认方法。默认方法使已经存在的接口可以修改而不会影响编译的过程。java.util.Collection中添加的额外方法就是最好的例子:stream(), parallelStream(), forEach(), removeIf()
Java 8 允许方法用 static 修饰。
// 可以查看下文中static的使用
static void a() {}
接口必须引用实现了这个接口的类对象。
interface A {}
class B implements A {}
A a = new B();
重载
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表,则视为重载。
// 华为的面试题中曾经问过这样一个问题 “为什么不能根据返回类型来区分重载”,快说出你的答案吧!
// 因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。如:
float max(int a, int b);
int max(int a, int b);
// 有时候编译器可以判断出要调用的是哪个方法。如:
int great = max(3, 4); // 可以判断出要调用的是 int max(int a, int b);
// 有时不能判断出,如:
void max();
int max();
当我们只调用 max(); 时就无法判断要调用什么了。
static
静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。它们的行为也并不具有多态性。
可以使用语法:父类名.静态方法调用隐藏的静态方法。
static的执行顺序
执行顺序
static块 > anonymous block > constructor function
直接上代码
//main函数的代码如下
public static void main(String[] args) {
new ForStatic();
System.out.println("------------------------");
new ForStatic(); // static块只会被执行一次,这次就不执行了
}
//类方法,被main方法调用
class ForStatic {
static {
System.out.println("111111111 Static 块");
}
{
System.out.println("2222222222 Anonymous block");
}
public ForStatic() {
System.out.println("3333333333 Constructor function");
}
static {
System.out.println("444444444 Static 块");
}
{
System.out.println("555555555 Anonymous block");
}
}
运行结果
111111111 Static 块
444444444 Static 块
2222222222 Anonymous block
555555555 Anonymous block
3333333333 Constructor function
------------------------
2222222222 Anonymous block
555555555 Anonymous block
3333333333 Constructor function
存在继承的情况下,初始化顺序为:
-
父类(静态变量、静态语句块)
-
子类(静态变量、静态语句块)
-
父类(实例变量、普通语句块)
-
父类(构造函数)
-
子类(实例变量、普通语句块)
-
子类(构造函数)
enum
- enum 的类型是一个类,其地位与 class、interface 相同。
- 枚举被设计成单例模式
- 所有的枚举类型都是 Enum 的子类。
- 枚举的构造器总是 private 的,即使省略了访问控制符,也还是 private。
public interface Gender {
void info();
}
public enum Size implements Gender { // 可以实现接口,但不能继承类
// SMALL,MEDIUM,LARGE;
SMALL("小"), // 自动 public static final 修饰
MEDIUM("中"),
LARGE("大"); // 枚举值必须声明在最前面
private final String chinese;
Size(String chinese) { // 枚举类型里有值,必须声明相同参数的构造器
this.chinese = chinese;
}
public String getChinese() {
return this.chinese;
}
public static void main(String[] args) {
for (Size size : Size.values()) { // values()方法
System.out.println(size.name() + " " + size.getChinese()); // SMALL 小
// Size.SMALL.name() == Size.SMALL == Size.SMALL.toString()
}
System.out.println(Size.SMALL.compareTo(Size.LARGE)); // -2
System.out.println(Size.SMALL.ordinal()); // 返回常量的位置(从0开始)
Size size = Enum.valueOf(Size.class, "SMALL"); // toString()的逆方法
Size.SMALL.info(); // 接口方法
}
@Override
public void info() { System.out.println("Gender在这里"); }
}
----------------------------------------------------------------------------
============================================================================
jad反编译后如下,其中,我删了一些没什么用的
jumpig/lee/test 代码在这个包下
public final class Size extends Enum
implements Gender
{
public static Size[] values()
{
return (Size[])$VALUES.clone();
}
public static Size valueOf(String name)
{
return (Size)Enum.valueOf(jumpig/lee/test/Size, name);
}
private Size(String s, int i, String chinese) // 这个构造器变了,有了 3 个参数
{
super(s, i);
this.chinese = chinese;
}
public static final Size SMALL;
public static final Size MEDIUM;
public static final Size LARGE;
private final String chinese;
private static final Size $VALUES[];
static // 静态代码块,一开始就加载
{
SMALL = new Size("SMALL", 0, "\u5C0F");
MEDIUM = new Size("MEDIUM", 1, "\u4E2D");
LARGE = new Size("LARGE", 2, "\u5927");
$VALUES = (new Size[] {
SMALL, MEDIUM, LARGE
}); // Size.info(); <---- 不行,通过构造器后才能使用 ---> Size.SMALL.info();
}
}
抽象方法
public enum Operation {
PLUS {
@Override
public double calculate(double x, double y) { return x + y; }
},
MINUS {
@Override
public double calculate(double x, double y) { return x - y; }
};
abstract double calculate(double x, double y);
public static void main(String[] args) { // main()方法
Operation.MINUS.calculate(9, 4);
}
}
- java.util 中添加了两个新类:
EnumMap和EnumSet。专门为枚举类型设计的。 - MyBatis Plus 里面 MatchSegment 类,在 enum 里使用了 lambda 表达式,很有意思
枚举里使用lambda
@FunctionalInterface
public interface MyISql { // 函数式接口
String getSql();
}
public enum MyKeyword implements MyISql {
ORDER_BY("ORDER BY"), // public static final MyKeyword ORDER_BY;
EXISTS("EXISTS");
private final String keyword;
MyKeyword(String keyword) { this.keyword = keyword; }
@Override
public String getSql() { return this.keyword; }
}
import java.util.function.Predicate;
public enum MySegment {
ORDER_BY(i -> i == MyKeyword.ORDER_BY), // public static final MyKeyword ORDER_BY;
EXISTS(i -> i == MyKeyword.EXISTS);
private Predicate<MyISql> predicate;
MySegment(Predicate<MyISql> predicate) { // 构造器,参数为lambda表达式
this.predicate = predicate;
}
}
--------------------------------------------------------------------
====================================================================
cfr反编译后:java -jar cfr-0.151.jar MySegment.class
public enum MySegment {
ORDER_BY(myISql -> myISql == MyKeyword.ORDER_BY),
EXISTS(myISql -> myISql == MyKeyword.EXISTS);
private Predicate<MyISql> predicate;
private MySegment(Predicate<MyISql> predicate) {
this.predicate = predicate;
}
}
常量池
常量池:相同的值只存储一份,节省内存,共享访问
基本类型的包装类:
- Boolean : true,false
- Byte: -128~127 , Character: 0~127
- Short, Int, Long: -128~127
- String: 字符串类型
- Float, Double: 没有缓存(常量池)
例子举起来
Integer i1 = 127;
Integer i2 = 127;
i1 == i2 (true) //Integer的范围从-128~127
Integer i3 = 128;
Integer i4 = 128;
i3 == i4 (false) //Integer的范围从-128~127
基本类型的包装类和字符串有两种创建方式
1. 常量式赋值创建,放在栈内存(将被常量化)
String name = "abc";
Integer a = 10;
2. new对象被创建后,放在堆内存(不被常量化)
Integer c = new Integer(10);
String d = new String("abc");
这两种对象创建的方式会放在不同的位置
例子如下
int i1 = 10;
Integer i2 = 10; // 自动装箱
i1 == i2 (true) // 自动拆箱,基本类型和包装类型比较,包装类型自动拆箱
Integer i3 = new Integer(10);
i1 == i3 (true) // 自动拆箱,基本类型和包装类型比较,包装类型自动拆箱
i2 == i3 (false)
// 两个对象比较,比较其地址
// i2是常量,放在栈内存常量池中,i3是new出来的,放在堆内存中
Integer i4 = new Integer(5);
Integer i5 = new Integer(5);
i1==(i4+i5) (true) i2==(i4+i5) (true) i3==(i4+i5) (true)
// i4+i5的操作会使i4,i5自动拆箱为基本类型
String s0 = "abcdef";
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
// s0,s1,s2都放在常量池中(栈内存) s3,s4(堆内存)
s1==s2 (true) s1==s3 (false) s3==s4 (false)
String s5 = s1 + "def"; //s1是变量,涉及到变量,编译器不优化
String s6 = "abc" + "def"; //都是常量,编译器自动优化为abcdef
String s7 = "abc" + new String ("def"); //设计的new对象,编译器不优化
s5==s6 (false) s5==s7 (false) s6==s7 (false) s0==s6 (true)
练习
题1
普通类HashMap中有下面一段,怎么可以没有实现全部的方法?
final class KeyIterator extends HashIterator implements Iterator<K> {
public final K next() { return nextNode().key; }
}
abstract class HashIterator {
// ...
}
因为 HashIterator 中已经实现了方法。
题2
// 可以直接返回 B 类
class Hei{
A me() { return new B(); }
}
class B implements A{
}
interface A {
}
对象克隆
拷贝只是引用相同
克隆相当于再创建一个对象

// Object 中的 clone 方法
protected native Object clone() throws CloneNotSupportedException;
如果拷贝对象中的数据字段都是基本类型或数值,当然没有问题。
但是克隆对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样原对象和克隆对象依然会共享一些信息。
浅拷贝:如下,没有克隆对象中引用的其他对象。

如果子对象是一个不可变的类(如:String),或者子对象包含不变的常量,或没有被更改,那当然是安全的。
不过,子对象可能是可变的,那就要重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象(在这里 hireDay 是一个 Date ,是可变的,所以它也必须克隆)。
// 浅拷贝的例子
class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException {
return (Employee) super.clone();
}
}
// Cloneable 是一个标记接口,这个接口指示一个类提供了一个安全的 clone 方法。这个接口没有方法,它的 clone 方法是从 Object 中继承来的。
// 如果一个对象请求克隆,但没有实现这个接口,就会生成一个检查型异常
深拷贝:如果要建立深拷贝,还要克隆对象中可变实例字段(这里:Date 是可变的)。
// 深拷贝的例子
class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
// 如果在一个对象上调用 clone ,但这个对象类没有实现 Cloneable 接口,Object 类的 clone 方法就会抛出一个 CloneNotSupportedException 异常。在这里,Date 类实现了 Cloneable 接口,所以不会抛异常,但编译器并不了解这一点,因此我们声明了这个异常:public Employee clone() throws CloneNotSupportedException。
所有的数组都有一个 public 的 clone 方法,可以用这个方法建立一个新数组。
浙公网安备 33010602011771号