java重拾-OOP
java重拾-OOP
类是具有相同属性和方法的一组对象的集合的抽象描述。
一个类可以包含:
- 字段(Field) 成员变量
- 方法(Method)
- 构造方法(Constructor)
Object类
即所有类的最基类
在创建新的类的时候,只暴露必要的接口,而隐藏其他所有不必要的信息,之所以要这么做,是因为如果这些信息对调用者是不可见的,那么创建者就可以随意修改隐藏的信息,而不用担心对调用者的影响。
- public
- private
- protected
组合
一个类可以作为另一个类的成员变量
继承
一个类可以以另一个类为基础, 修改其方法(重写), 扩展其方法
多态
父类指针可以指向子类对象, 实现相同接口的一系列类都可以具有相同的调用方式
packaeg
每个类的绝对定位为package.classname
例如:
小明的 Person
类存放在包 ming
下面,因此,完整类名是 ming.Person
;
小红的 Person
类存放在包 hong
下面,因此,完整类名是 hong.Person
;
小军的 Arrays
类存放在包 mr.jun
下面,因此,完整类名是 mr.jun.Arrays
;
JDK 的 Arrays
类存放在包 java.util
下面,因此,完整类名是 java.util.Arrays
。
在定义 class
的时候,我们需要在第一行声明这个 class
属于哪个包。
小明的 Person.java
文件:
package ming; // 申明包名ming
public class Person {
}
小军的 Arrays.java
文件:
package mr.jun; // 申明包名mr.jun
public class Arrays {
}
注意: 同一个package下的public才可以直接访问
// Person.java
package ming;
// 导入mr.jun包的所有class:
import mr.jun.*;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
我们一般不推荐这种写法,因为在导入了多个包后,很难看出 Arrays
类属于哪个包。
还有一种 import static
的语法,它可以导入一个类的静态字段和静态方法:
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
import static
很少使用。
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:
- org.apache
- org.apache.commons.log
- com.tobebetterjavaer.sample
子包就可以根据功能自行命名。
要注意不要和 java.lang
包的类重名,即自己的类不要使用这些名字:
- String
- System
- Runtime
- ...
要注意也不要和 JDK 常用类重名:
- java.util.List
- java.text.Format
- java.math.BigInteger
- ...
变量
- 局部变量
就是方法内的变量, 存于栈, 初始化后才能访问
- 成员变量
类内的变量, 可以不初始化, 存于堆, 可以用修饰符限定访问权限, 生命周期与类相同
- 静态变量
直接存于内存中特定的地址, 任意地方访问都是同一个变量
方法
访问权限:它指定了方法的可见性。Java 提供了四种访问权限修饰符:
- public:该方法可以被所有类访问。
- private:该方法只能在定义它的类中访问。
- protected:该方法可以被同一个包中的类,或者不同包中的子类访问。
- default:如果一个方法没有使用任何访问权限修饰符,那么它是 package-private 的,意味着该方法只能被同一个包中的类可见。
静态方法
static定义的, 只要类加载了即可调用
实例方法
对象实例化才可以调用
抽象方法
抽象类限定, 被继承并重写后才可调用
可变参数
其实也很简单。当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法。
这就是为什么可以使用数组作为参数来调用带有可变参数的方法的根本原因。代码如下所示。
public static void main(String[] args) {
print(new String[]{"沉"});
print(new String[]{"沉", "默"});
print(new String[]{"沉", "默", "王"});
print(new String[]{"沉", "默", "王", "二"});
}
public static void print(String... strs) {
for (String s : strs)
System.out.print(s);
System.out.println();
}
native 方法
JNI 的缺点:
- 程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。
- 程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了 Java 和 C/C++ 之间的耦合性。
native 用来修饰方法,用 native 声明的方法表示该方法的实现在外部定义,可以用任何语言去实现它,比如说 C/C++。 简单地讲,一个 native Method 就是一个 Java 调用非 Java 代码的接口。
native 语法:
- 修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。
- 不能用 abstract 修饰,也没有方法体,也没有左右大括号。
- 返回值可以是任意类型
构造方法
当一个类被实例化的时候,就会调用构造方法
只有在构造方法被调用的时候,对象才会被分配内存空间
每次使用 new
关键字创建对象的时候,构造方法至少会被调用一次
构造方法必须符合以下规则:
- 构造方法的名字必须和类名一样;
- 构造方法没有返回类型,包括 void;
- 构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。
可以使用权限修饰符, 代表该构造函数的使用地点
代码初始化块
- 类实例化的时候执行代码初始化块;
- 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
- 代码初始化块里的执行顺序是从前到后的。
抽象类
实现了一些基本方法的接口类, 不允许实例化, 将抽象方法重写后可以实例化
- 抽象类不能被实例化。
- 抽象类应该至少有一个抽象方法,否则它没有任何意义。
- 抽象类中的抽象方法没有方法体。
- 抽象类的子类必须给出父类中的抽象方法的具体实现,除非该子类也是抽象类。
接口
public interface Electronic {
// 常量
String LED = "LED";
// 抽象方法
int getElectricityUse();
// 静态方法
static boolean isEnergyEfficient(String electtronicType) {
return electtronicType.equals(LED);
}
// 默认方法
default void printDescription() {
System.out.println("电子");
}
}
接口中定义的变量会在编译的时候自动加上 public static final
修饰符
接口中允许有静态方法,比如说上例中的 isEnergyEfficient()
方法。
静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用,比如说 Electronic.isEnergyEfficient("LED")
。
接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。
接口中允许定义 default
方法,比如说上例中的 printDescription()
方法,它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。
允许在接口中定义默认方法的理由很充分,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 default
方法的帮助下,我们就必须挨个对实现类进行修改。
接口不允许直接实例化需要定义一个类去实现接口
接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在 java.io
包下。
public interface Serializable {
}
Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。
不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为
接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错
接口的变量是隐式 public static final
(常量),所以其值无法改变
Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的
如果有两个类共同继承(extends)一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候,编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题,见下图。
实现多态
不同的类实现同一个接口, 具有相同的调用方式, 相同的能力, 实现方式不同
策略模式
相同的 算法/行为/性质 封装到同一个function中, 不同的实现者给出不同的实现
可以在调用者无感的情况下, 改变其实现
适配器模式
加一层, 把接口不同的转换一下
秒懂设计模式之适配器模式(Adapter Pattern) - 知乎
工厂模式
本质上是套了两层的策略模式
内部类
成员内部类
成员内部类是最常见的内部类
定义在类内
class Wanger {
int age = 18;
class Wangxiaoer {
int age = 81;
}
}
成员内部类可以无限制访问外部类的所有成员属性。
public class Wanger {
int age = 18;
private String name = "沉默王二";
static double money = 1;
class Wangxiaoer {
int age = 81;
public void print() {
System.out.println(name);
System.out.println(money);
}
}
}
外部类想要访问内部类的成员,必须先创建一个成员内部类的对象,再通过这个对象来访问
public class Wanger {
int age = 18;
private String name = "沉默王二";
static double money = 1;
public Wanger () {
new Wangxiaoer().print();
}
class Wangxiaoer {
int age = 81;
public void print() {
System.out.println(name);
System.out.println(money);
}
}
}
静态内部类
它不需要外部类的实例就可以被创建和使用。静态内部类不能访问外部类的非静态成员(实例变量和实例方法),但可以访问外部类的静态成员(静态变量和静态方法)以及它自己的成员
不知道干嘛的, 没想到有什么场景
局部内部类
定义在一个方法或者一个作用域里面的类
局部内部类的生命周期仅限于作用域内
public class Wangsan {
public Wangsan print() {
class Wangxiaosan extends Wangsan{
private int age = 18;
}
return new Wangxiaosan();
}
}
局部内部类就好像一个局部变量一样
所以不能使用权限修饰
匿名内部类
没有显式的类名, 仅是继承某个类或者接口并实现, 将子类继承父类、重写父类中的方法、创建子类对象的步骤合并为一步,从而减少了代码量, 增加可读性
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
t.start();
}
}
匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写
- 内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
- 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
- 创建内部类对象的时刻并不依赖于外部类对象的创建。
- 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 内部类提供了更好的封装,除了该外围类,其他类都不能访问。
封装
对类的成员变量和基于数据的操作封装在一起,使其构成一个不可分割的独立实体
说白了, 就是把变量之间的操作搞成一个方法提供出去, 简化外部调用
继承
子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法
使得复用以前的代码非常容易
直接的类继承无法实现多继承
使用内部类或者接口或者多重继承来实现多继承
重写(Override)
方法重写也就是子类中出现和父类中一模一样的方法(包括返回值类型,方法名,参数列表),它建立在继承的基础上。你可以理解为方法的外壳不变,但是核心内容重写。
在这里提供一个简单易懂的方法重写案例:
class E1{
public void doA(int a){
System.out.println("这是父类的方法");
}
}
class E2 extends E1{
@Override
public void doA(int a) {
System.out.println("我重写父类方法,这是子类的方法");
}
}
其中 @Override
注解显示声明该方法为注解方法,可以帮你检查重写方法的语法正确性,当然如果不加也是可以的,但建议加上。
重载(Overload)
如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。
重载可以通常理解为完成同一个事情的方法名相同,但是参数列表不同其他条件也可能不同。一个简单的方法重载的例子,类 E3 中的 add()方法就是一个重载方法。
class E3{
public int add(int a,int b){
return a+b;
}
public double add(double a,double b) {
return a+b;
}
public int add(int a,int b,int c) {
return a+b+c;
}
}
访问修饰符
子类重写继承的方法时,不可以降低方法的访问权限
静态方法不允许被重写
final 方法可以被继承,但是不能被重写
final 类不能被继承
Object类
所有类的根类:Object(java.lang.Object)类,如果一个类没有显式声明它的父类(即没有写 extends xx),那么默认这个类的父类就是 Object 类,任何类都可以使用 Object 类的方法,创建的类也可和 Object 进行向上、向下转型
初始化
父子类初始化先后顺序为:
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造方法
- 子类中普通成员变量和代码块,子类的构造方法
静态>非静态,父类>子类,非构造方法>构造方法
多态
同一个类的对象在不同情况下表现出来的不同行为和状态
- 子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
- 子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
- 可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。
this&super
this就是自身对象的指针, 可以
- 指向当前对象
- 调用当前类的方法
this()
可以调用当前类的构造方法;- this 可以作为参数在方法中传递;
- this 可以作为参数在构造方法中传递;
- this 可以作为方法的返回值,返回当前类的对象
super就是父类的指针, 用于专门调用父类的方法
每当创建一个子类对象的时候,也会隐式的创建父类对象,由 super 关键字引用
不需要显式调用super()对父类进行初始化, 编译器会做的
不可变类
即无setter方法 也不会有变更其成员变量的方法, 相当于每个变量都是final的
如string类
在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗
注解
枚举
简单理解就是给字符和数字建一个map, 所有使用枚举类的都有相同的key value
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public final class PlayerType extends Enum
{
public static PlayerType[] values()
{
return (PlayerType[])$VALUES.clone();
}
public static PlayerType valueOf(String name)
{
return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
}
private PlayerType(String s, int i)
{
super(s, i);
}
public static final PlayerType TENNIS;
public static final PlayerType FOOTBALL;
public static final PlayerType BASKETBALL;
private static final PlayerType $VALUES[];
static
{
TENNIS = new PlayerType("TENNIS", 0);
FOOTBALL = new PlayerType("FOOTBALL", 1);
BASKETBALL = new PlayerType("BASKETBALL", 2);
$VALUES = (new PlayerType[] {
TENNIS, FOOTBALL, BASKETBALL
});
}
}
Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。”
- 要继承 Enum 类;
- 要写构造方法;
- 要声明静态变量和数组;
- 要用 static 块来初始化静态变量和数组;
- 要提供静态方法,比如说
values()
和valueOf(String name)
由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等
如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。”我继续说。
enum PlayerType {
TENNIS("网球"),
FOOTBALL("足球"),
BASKETBALL("篮球");
public String name;
PlayerType(String name) {
this.name = name;
}
}
public class Main{
public static void main(String[] args) throws IOException {
PlayerType a = PlayerType.FOOTBALL;
System.out.println(a);
System.out.println(a.name);
a.name = "321123";
System.out.println(a.name);
}
//
//FOOTBALL
//足球
//321123
//
}
EnumSet EnumMap 就是针对枚举类实现的一些数据结构, 可以直接调用方便使用
Java枚举:小小enum,优雅而干净 | 二哥的Java进阶之路
使用枚举类实现单例是最好的方式