1、对象内存空间分布图
① 、每创建一个对象都会在堆内存中开辟一块空间,并且这块空间中具有和类(模板)中一样的成员。
② 、每一个对象都被栈中的一个变量所指向,所以操作栈中的变量(s)就如同操作堆中的对象。
③ 、s.name = "小王";其实是把字符串值赋值给s变量指向的堆中的name字段上的,而不是设置给类的,所以我们在分析代码的时候,看到new Student()对象应该立马想到在堆中有一个对象。
2、构造方法要点
1、构造方法名与类的名字一模一样
2、没有返回值类型修饰
3、 构造方法可以创建对象。
4、因为构造方法的调用是在 对象数据区 赋值工作 之前执行的,所以 构造方法常用来给对象的属性进行 初始化 工作
5、给对象开辟内存空间时会自动调用
6、当类中没有自定义有参数构造方法和无参数构造方法时,程序中会默认隐藏一个 无参构造方法 ( public className(){ } )
当程序中自定义了一个有参数数构造方法或者无参数构造方时,那么隐藏的那个无参构造方法就被覆盖(不存在了)
7、构造方法可以重载,不可以重写。因为构造方法不能继承到子类。
3、匿名对象
概念: 没有名字的对象,创建对象时没有对应类型的变量去接收。
优点:
匿名对象只能使用一次, 匿名对象调用完毕就是垃圾,可以被垃圾回收器回收,提高内存使用效率。
匿名对象可以作为实际参数传递。
package cn.manman.com; /* * 匿名对象的应用场景: * A:调用方法,仅仅只调用一次的时候; * 优势是:匿名对象调用完就被垃圾回收器回收。提高内存使用效率; * B:匿名对象可以作为实际参数传递; * */ public class NoNameDemo { public static void main(String[] args) { //带名字的调用 Student student=new Student(); student.show(); student.show();//这里的对象和上一个对象是同一个对象; //匿名对象 new Student().eat(); new Student().eat();//这里其实是重新创建了一个新的对象在调用 } } class Student{ public void show(){ System.out.println("我们爱学习!"); } public void eat(){ System.out.println("爱吃火锅!"); } }
4、对象的生命周期
开始:new的时候就开始了;
结束(说法1,常见的说法) :当对象失去所有的引用(没有变量再指向它了(没有栈空间的变量去在存储它在堆空间的地址)- 相当于失联了,我们无法再使用它了)-- 就是死亡了;(垃圾回收器 并不是立刻进行回收)
结束(说法2) : 对象真正的被销毁(对象会在堆里面占用内存,当把对象的内存空间回收了),Java有自动垃圾回收机制;
5、值传递和引用传递的理解
先看案例:
public class StringBase { public static void main(String[] args) { int c = 66; //c 叫做实参 String d = "hello"; //d 叫做实参 StringBase stringBase = new StringBase(); stringBase.test5(c, d); // 此处 c 与 d 叫做实参 System.out.println("c的值是:" + c + " --- d的值是:" + d); } public void test5(int a, String b) { // a 与 b 叫做形参 a = 55; b = "no"; } }
运行结果: c的值是:66 --- d的值是:hello
1、值传递
在方法的调用过程中,实参把它的实际值传递给形参,此传递过程就是将实参的值复制一份传递到函数中,这样如果在函数中对该值(形参的值)进行了操作将不会影响实参的值。因为是直接复制,所以这种方式在传递大量数据时,运行效率会特别低下。
2、引用传递
引用传递弥补了值传递的不足,如果传递的数据量很大,直接复过去的话,会占用大量的内存空间,而引用传递就是将对象的地址值传递过去,函数接收的是原始值的首地址值。在方法的执行过程中,形参和实参的内容相同,指向同一块内存地址,也就是说操作的其实都是源数据,所以方法的执行将会影响到实际对象。
面试的时候简单描述:
- 基本数据类型传值,对形参的修改不会影响实参;
- 引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象。
- String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。
6、静态修饰符static
1、可以去修饰 成员变量(属性或者字段),不可以修饰 类(外部类),构造方法,局部变量
2、有static修饰的字段和方法,我们可以用字段所在的类的 类名.字段 类名.方法去访问,没有用static修饰的字段和方法,只能用实例去访问它,即创建对象去调用。
注意:
1、静态的属性放在 类的数据区 内, 所以 每个对象访问静态属性的时候 都是去 类的数据区中访问的静态属性的值
2、对象的数据区 中不包含静态属性。
3、一般情况下,类中的全局常量 用static修饰的较多,而类中的 字段 很少使用static修饰
static修饰的属性和方法在什么时候加载到类数据区里?
当程序编译时,该类中用到的所有类,这些类都会编译,然后这些的静态属性和静态方法都会被加载到类的数据区里去,看下图:
7、访问权限修饰符
作用:主要用来修饰类中的成员(属性,普通方法,构造方法),也可以去修饰类
private :该修饰符只能在本类中访问
default(缺省不写):在不同包下面不可以访问
protected:在不同包下面不可以访问
public:在任何地方都可以访问
注意:
1、public 、默认的(缺省不写) 这两种可以去修饰类(外部类,内部类)
2、private 和protected 不能去修饰外部类
3、所有的访问权限修饰符都不可以去修饰 局部变量
总结:
公共的修饰(public):成员方法 字段(可以去修饰但是一般情况下用private修饰),构造方法, 内部类 ,类
私有的修饰(private): 字段,,成员方法(一帮情况成员方法用public修饰),构造方法 ,内部类
8、javabean是一个标准的java类
要求:
1.类必须要是public修饰的
2.类中的属性必须是private修饰的
3.类中必须提供一个无参数构造器
4.每个私有化的属性必须提供一组getter/setter方法
注意: 1.Boolean类型(布尔的包装类),生成的get方法是get开头的(建议使用这个).
2.boolean类型,生成的get方法是is开头的 (用这个最好重写 getXxx() 格式的方法,因为涉及到反射,反射一般会默认调取对象的get方法)
9、理解this关键字
用法:
1、在方法中出现的this代指调用该方法的对象
2、this可以解决属性与局部变量同名时的冲突
Public Class Student { String name; //定义一个成员变量name private void SetName(String name) { //定义一个参数(局部变量)name this.name=name; //将局部变量的值传递给成员变量 } }
3、this可以调用构造方法(注意:this必须是构造器中的第一条语句)
public class Student { //定义一个类,类的名字为student。 public Student() { //定义一个方法,名字与类相同故为构造方法 this(“Hello!”); } public Student(String name) { //定义一个带形式参数的构造方法 } }
4、this可以作为方法的返回值,返回的是调用this所处方法的那个对象的引用,更简单点说,就是谁调用返回的就是谁。
由于返回的是对象引用,所以this不能用在静态成员方法中,只能在非静态成员方法中出现。
详细参考:https://www.cnblogs.com/chanchan/p/7812166.html
5、this可以作为方法的调用时的实参
6、this在自定义类型中的使用(同桌问题,一定要理解)
class People { private String name; private People friend; public void setName(String name){ this.name = name; } public String getName(){ return this.name; } public void setFriend(People friend){ if(this.getFriend() == friend){ return; } this.friend = friend; friend.setFriend(this);//此种方式注意死循环 } public People getFriend(){ return this.friend; } } public class TestPeople { public static void main(String[] args) { People p1 = new People(); p1.setName("小王"); People p2 = new People(); p2.setName("小张"); //设置朋友关系 p1.setFriend(p2); System.out.println(p1.getName()+"的朋友是:"+p1.getFriend().getName()); //p2.setFriend(p1); System.out.println(p2.getName()+"的朋友是:"+p2.getFriend().getName()); } }
10、方法的重写、方法的重载
方法的重写概念:将父类的方法复制到子类重新定义方法体内的代码的过程,子类中拥有一个和父类完全一样的方法,将这两个方法称为重写。
方法重写的要求:
1、保证子类方法和父类方法的方法签名(方法名+参数列表)一致,其中形参的名称是否一样无所谓
2、(访问权限等级高低 public > 默认的 > private) 子类的方法的访问修饰符的等级 等于或者大于 父类的方法的访问修饰符
3、private修饰的方法不能被重写(private修饰的成员方法不能被继承到子类)
4、static修饰的方法不能被重写(static修饰的方法存在类的数据区,不在对象数据区)
5、子类中重写的方法的返回值类型 与 父类方法返回值类型 相同或者是父类的返回值类型的子类(不是java中的等级高低)
注意:
1、在编译阶段验证是否覆写: 在子类方法上面加 @Override ,用来检测子类中的方法是否重写父类的方法,让编译器来检查,如果是正确的覆写,编译通过,否则编译报错
2、子类方法和父类方法完全一样,也是方法的重写。
方法的重载概念: 在同一个类中方法名必须相同,形参列表中的顺序,个数,类型不同的两个或者两个以上的方法称为方法的重载,且与返回值类型、访问修饰符无关。
11、Object类、toString()、equals()介绍
注意:★(所有的引用数据类型的对象都属于Object类型)
1、Object类位于java.lang包下面,使用时不需要导包
2、所有的引用数据类型的对象都可以使用Object类里的方法
3、基本数据类型变量 不可以使用Object类里的方法
toString()
哪个对象调用toString()方法,该方法就返回该对象的字符串表示形式
如果对象所属的类中没有重写toString,那么对象访问的是Objet类中toString方法
如果对象所属的类中重写了toString ,那么对象访问的是他自己本身类中重写的toString方法
注意:在自定义类中重写toString()的方法是因为打印对象名时,不想看到类似这样"Student@56dd8cc"的值,而是想清楚的知道对象的属性值是什么。
1、Object 和String 中的toString()的源码
// Object 中的toString(); public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } // String 中的toString(); public String toString() { return this; }
equals()
2、Object中的equals怎么定义的?
//lang包-- Object.java 中的equals(); public boolean equals(Object obj) { return (this == obj); }
3、String 中的equals怎么定义的?
//lang包-- String.java 中的equals(); public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { //强制转换,装箱。 String anotherString = (String)anObject; // value指前面的需要比较的字符串 int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
4、自定义类中的equals方法怎么定义?
//自定义类中的equals方法 public boolean equals(Student stu) { if(((this.getName()).equals(stu.getName())) && ((this.getStuId())==(stu.getStuId()))){ return true; } return false; } /* 1.编译器看到的obj的类型是 Object ,Object中是没有name和age的,编译报错 但是,我们知道实际调用的时候obj变量中装的是一个学生对象, 应该明确的告诉编译器这个obj是学生对象---》 obj强制转换成 Student*/ /* 2.this.name 其实类型是String 是引用类型(非常特殊的引用类型) ; 使用== 比较有风险(可能比较的是地址),应该比较字符串的字面值 String类在设计的时候就于已经覆写了Object中的equals方法,比较规则就是使用的字面值进行比较 所以 上面this.name == s.getName() 应该调用String类中的equals方法*/ public boolean equals(Object obj) { Student s = (Student)obj; if(this.name.equals(s.getName())&&this.age == s.getAge()){ return true; } return false; } /* 3.上面的代码 if中的条件 本身就是一个逻辑运算,逻辑运算表达式的结果值就是一个boolean 所以没有必要写if else*/ public boolean equals(Object obj) { Student s = (Student)obj; return this.name.equals(s.getName())&&this.age == s.getAge(); }
★★:5、为什么调用println方法会 自动调用toString方法?
//io包--PrintStream.java 里面的println(); public void println(Object x) { String s = String.valueOf(x); synchronized (this) { print(s); newLine(); } } //lang包--String.java 里面的valueOf(); public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); }
★★★★★6、== 和 equals 都是比较是否相等,请问它们到底有什么区别呢?
1 、==
基本数据类型: 比较的就是值是否相等;
引用数据类型: 比较的是对象的地址是否一样;(排除特殊 String)
2 、equals (最初定义在根类Object中的)
基本数据类型 : 不能够使用! 基本数据类型不是对象,不能够调用Object中的方法
引用数据类型 : 在Object的源码中定义的就是==进行比较比较,比较的是对象的地址是否一样
而在String类型和包装类型(Integer)都重写了equals方法,比较是对应的值。
在实际开发中,我们一般比较对象都是通过对象的属性值进行比较(一般比较对象的地址没有多大用处),所以我们会经常覆写Object中的此方法,把自己的规则写在方法里面;
12、super关键字
作用:调用父类中的成员(属性,成员方法,构造方法)
1、在子类的方法中使用,代指父类对象,可以去调用父类属性和方法
2、在子类的构造方法 里访问父类的构造方法 用super() 调用无参父类构造方法。用super( 参数列表) 调用有参父类构造方法。
3、子类的构造器中的第一行代码会默认隐藏一个 super() ,当子类构造器中书写了super()或者this()的 (有参无参 )时,构造器中的隐藏的super()会被覆盖。
4、super() 和 this() 表达式 必须是构造器中的第一条语句,导致了super(),this()不能在构造器中同时使用,
使用场景:
1、super可以去访问父类中的非私有化的成员
2、调用父类构造方法(在子类构造器中 对父类私有化的属性进行初始化值)
3、子类构造器中第一条语句默认存在super();
13、final 关键字
final 修饰的类不能有子类,即不能被继承。
final 不能修饰构造方法,final修饰的方法不能被重写。
final 修饰的常量要初始化,且不能更改。(final修饰的常量要么直接赋值,要么使用有参或者无参的构造方法进行初始化,使用setter方法赋值会报错)
final 修饰的对象的引用不能改变。列如数组和对象的变量引用。
14、封装
思想:就是把对象的属性和行为(方法)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,对外只提供能使用的方式(getter(),setter(),起到了安全性的作用。
15、继承
子类继承父类中某些成员 , 父类(超类,基类,根类),子类(派生类,拓展类)
泛化:在多个子类的基础上面抽取共有的属性和行为到一个父类中去。
特化:在一个父类 的基础上拓展子类的特有属性和行为,生成一个新的子类。
原则:父类中存在共性,子类中存在特性。
优势: 提高代码的复用性。
子类可以去继承哪些成员?1、非私有化的成员(字段,方法) 2、private修饰的成员以及构造方法不能被继承
继承的特点
1、在java程序中继承只允许单继承(一个子类只允许有一个直接父类)
2、在java程序中允许每个类之间多重继承 eg: A extends B, B extends C C extends D
3、如果类都没有去直接继承另外一个类,那么该类会默认继承 超级基类(Object类)
16、多态
1、概念:一种事物(对象)有多种形态(类型)可以屏蔽不同的子类之间的实现差异,多态是指通过指向父类的指针,来调用在不同子类中实现的方法。
多态形式创建对象:
定义类型 对象名 = new 实际类型( );
定义类型:父类, 实际类型是:子类 ,也就是定义了一个 指向子类 的 父类引用类型 的 对象
2、多态方法调用编译和运行时的过程。
上面两句代码的编译,运行过程:
编译 : 第三行, 如果Animal是Person的父类,那么编译通过,否则编译报错;
第四行, (编译器把p1看成是Animal) 编译的时候会到p1的编译类型中找是否有eat方法,如果没有,会继续向p1的编译类型的父类中一直向上找,如果都没有找到,编译报错, 如果找到了编译通过
(不会向下到子类中去找, 找的时候就是编译的时候是不会执行代码的)
运行 : 第四行 , 先到运行时类型(Person)找eat方法,如果找到就执行,否则就向上到父类中找并执行
注意:
1、多态形式创建的对象可以调用哪些属性和方法,取决于定义类型
2、多态形式创建的对象的实际类型(子类)中,方法发生了重写,那么该对象调用的是子类中的方法
实际开发中一般不会在子类中定义一个和父类同名的字段,如果是有这样的情况存在,如果是使用的父类的对象,取值是父类里面的值; 如果子类对象,取值是子类里面的值;
也就是父类类型的引用可以调用父类中定义的所有属性和方法
17、引用类型转换
小转大(向上转型):将子类对象 赋值给 父类对象的变量保存的过程,此时自动转换
大转小(向下转型):将父类对象 赋值给 子类对象的变量的过程,但此时需要强转
注意一般在开发中遇到向下转型时,都会对该对象的实际类型进行判断
boolean is = obj(目标对象) instanceof Type(目标对象类型)
18、抽象类和接口
理解: 我们都知道在面向对象的领域一切都是对象,同时所有的对象都是通过类来描述的,但是并不是所有的类都是来描述对象的。如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来支撑它,那么这样的类我们称它为抽象类。
1、使用abstract修饰的类是抽象类,抽象类本质也是一个类
2、类中有的成员 抽象类都可以有(字段 方法 构造方法),此外抽象类还比普通类多一个抽象方法, 故抽象类不允许创建对象。当然,抽象类中可以没有抽象方法。
3、抽象方法:用abstract修饰的方法 ,它没有方法体,并且定义时最后结束加;而且抽象方法 必须存于抽象类中(接口也可以),不能够放在普通类中。
4、一般将抽象的类作为父类, (普通)子类继承抽象父类,必须重写所有父类中的抽象方法。当然,如果子类也是抽象类,可以不用去重写父类中的抽象方法。
注意:abstarct 不能修饰属性,因为属性没有必要实现。
接口引用指向实现类的对象 比如:List list = new ArrayList();
接口 理解:接口本身就不是类, 接口是抽象类的延伸,接口是用来建立类与类之间的协议,它只提供一种形式,而没有具体的实现。
同时实现该接口的实现类 必须 要 实现该接口的所有方法,通过使用implements关键字,他表示该类在遵循 某个或 某组 特定的接口。
java为了保证数据安全是不能多重继承的,也就是说继承只能存在一个父类,但是接口不同,一个类可以同时实现多个接口,
不管这些接口之间有没有关系,所以接口弥补了抽象类不能多重继承的缺陷,但是推荐继承和接口共同使用,因为这样既可以保证数据安全性又可以实现多重继承。
接口定义: interface 接口名{}
接口内部可以有哪些成员--参考类
字段 全部都是全局常量(public static final修饰), 故可以直接调用
方法 全部都是抽象方法(缺省修饰 public abstract)(接口中可以没有抽象方法,但是没意义)
抽象方法需要子类类覆写才有意义,而static final修饰的方法都不能够被覆写,接口中的方法不可以用 static ,final修饰
接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法; 构造方法 没有!
注意
1.接口主要强调的是功能,必须要有实现类去实现(实现的时候注意public修饰符)
2.接口相当于实现类的父类(多态时体现),类可以去实现多个接口,类与类单继承,一个接口可以去继承多个接口,但接口不可以去实现接口 。
接口和抽象的区别:
1.接口概念
2.抽象概念
3. 相同点: 1. 抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2. 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类还只能是抽象类。
同样,一个类实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
4. 不同点
1. 抽象层次不同。抽象类是对类抽象,而接口是对行为的抽象。即 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
2. 跨域不同。抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。
例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!
所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。
3.设计层次不同。对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点,形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
19、谈谈你对面向对象的理解
面向对象是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。
面向对象的编程是以对象为中心,以 消息为驱动,所以程序=对象+消息。
面向对象有三大特性,封装、继承和多态。
封装就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化。这样做使得代码的复用性更高。
继承则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类--有父类的行为和属性,也有自己特有的行为和属性。这样做扩展了已存在的代码块,进一步提高了代码的复用性。
如果说封装和继承是为了使代码重用,那么多态则是为了实现接口重用。多态的一大作用就是为了解耦--为了解除父子类继承的耦合度。如果说继承中父子类的关系式IS-A的关系,那么接口和实现类之之间的关系式HAS-A。简单来说,多态就是允许父类引用(或接口)指向子类(或实现类)对象。很多的设计模式都是基于面向对象的多态性设计的。
总结一下,如果说封装和继承是面向对象的基础,那么多态则是面向对象最精髓的理论。掌握多态必先了解接口,只有充分理解接口才能更好的应用多态。
20、面向对象和面向过程的区别?
做菜为例,其实面向过程就好像你是个厨师,要自己炒菜,所以要讲究步骤,
而面向对象就好像你是个食客,你只要通知厨师作菜,即发一个消息就可以了,至于厨师怎样作菜,是不用知道的。
---------------------------------------------------
两句话:
面向过程是一种以事件为中心的编程思想。就是分析出解决问题所需的步骤,然后用函数把这些步骤实现,并按顺序调用。(一种自顶向下的编程。)
面向对象是以“对象”为中心的编程思想。(自下向上先建立抽象模型然后再使用模型)。
21、面向对象开发的六个基本原则,迪米特法则
单一职责:一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不设计与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。
开放封闭:软件实体应当对扩展开放,对修改关闭。要做到开闭有两个要点。第一、抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;第二、封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而混乱。
里式替换:任何时候都可以用子类型替换掉父类型。子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。
依赖倒置:面向接口编程(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体的类型,因为抽象类型可以被它的任何一个子类型所替代)。
合成聚合复用:优先使用聚合或合成关系复用的代码。
接口隔离:接口要小而专,绝不能大而全。臃肿的接口是对接口的污染。既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高内聚的。
迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。
项目中用到的原则:单一职责、开放封闭、合成聚合复用(最简单的例子就是String类)、接口隔离。
22、枚举
当我们遇到 属性值是固定值个数(属性值有确定范围)正常来可以使用单例模式,我们在类中使用单例模式需要创建本类的对象,当我们声明多个对象时,public static final及构造器就是重复性的代码,而枚举简化了创建本类对象时的格式,从而简化了单例模式的声明的方式。即枚举能够解决属性值是固定个数的问题
看下面代码使用单利模式解决属性值确定范围的问题。显然重复性代码居多。
// 以知人的性别只有男女之分,固属性值固定。 public class Gender { private Gender(){} private static Gender man = new Gender(); private static Gender woman = new Gender(); //外部通过方法获取私有的对象 public static Gender getMan() { return man; } public static Gender getWoman() { return woman; } //重写toString方法 public String toString(){ if(this == man){ return "男"; }else if(this == woman){ return "女"; } return null; } }
public class Student { private Gender gender; public Gender getGender() { return gender; } public void setGender(Gender gender) { this.gender = gender; } }
public class Test { public static void main(String[] args) { Student s = new Student(); s.setGender(Gender.getMan()); //打印对象 System.out.println(s.getGender()); //男 } }
看下面代码使用 枚举 解决 季节这个属性值固定问题
public enum Season { spring("春",1),summer("夏",2),autumn(),winter("冬",4); private String name = "哈哈"; //初始值 private int index; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } // 私有的有参的构造方法 private Season(String name,int index){ this.name = name; this.index = index; } //私有的无参构造方法 ,这样枚举对象里面可以不用传值。 private Season(){} //重写toString(); public String toString(){ return this.name+""+this.index; } }
public class Test { public static void main(String[] args) { Season s = Season.spring; //通过枚举类名.对象名访问 固定不变的属性(对象) System.out.println(s); Season[] ss = Season.values();//获取枚举类中定义的所有的对象,返回一个枚举类型的对象数组 for (Season i : ss) { System.out.println(i.getName()+","+i.getIndex()); } } } 结果为: 春1 春,1 夏,2 哈哈,0 冬,4
23、组合关系
①、组合是(has-a)关系,而继承则是(is-a)关系;
②、 组合关系在运行期决定,而继承关系在编译期就已经决定了。
③、 组合是在组合类和被包含类之间的一种松耦合关系,而继承则是父类和子类之间的一种紧耦合关系。
④ 、当选择使用组合关系时,在组合类中包含了外部类的对象,组合类可以调用外部类必须的方法,而使用继承关系时,父类的所有方法和变量都被子类无条件继承,子类不能选择。
24、单例模式 (介绍五种) 饿汉模式 懒汉模式 双重检测锁模式 枚举 静态内部类模式
单例类必须自己创建自己唯一的实例(对象)
1、饿汉模式
//饿汉模式 public class Student { //私有化无参的构造方法,其他类就不可以随便创建对象了。 private Student(){}; //私有的,静态的对象,创建一次地址就不会改变,必须通过方法来访问 private static Student s = new Student(); //静态的方法,外部直接使用 类名.方法 调用 public static Student getInstance(){ return s; } }
2、懒汉模式
//懒汉模式 public class Teacher { private Teacher(){}; private static Teacher t = null; public static Teacher getInstance(){ //if语句为了 防止在堆内存空间创建不同的对象 而 违反了单例原则 if(t == null){ t = new Teacher(); } return t; } }
二者比较:
1 单例模式的类也是一个普通的类,其中也可以有其他的字段 方法等。。。
2 上面代码中,s对象是Student类被加载(把类放到JVM的过程中)的时候创建的
3 如果Student类中其他的字段和方法很多。。。,创建对象的过程比较长,类加载会比较慢
有可能加载之后很长时间其实都没有人来获得对象,浪费堆内存空间,这就是饿汉模式
4 在类加载的时候先不创建对象,而是在有人第一次来调用方法获得对象的时候才创建一个对象,
之后需要保存起来,以后再有人调用就不用创建对象。这就是懒汉模式
总结:
饿汉模式,类加载的时候效率低,获取单例对象时效率高,线程安全
懒汉模式,类加载的时候效率高,获取单例对象时效率低,线程不安全
3、懒汉模式的非线程安全问题的解决方法:双重检测锁模式,首先使用同步代码块,同步方法,效率低下;使用DCL(Double-Check Locking)双检查锁机制-
双重判空的的意义:对于person1存在的情况,就直接返回。当person1为null并且同时存在两个线程调用getPerson1()方法时,它们都将通过第一重的person1==null的判断。
然后由于类锁机制,这两个线程只有一个可以获得锁并进入,另一个在外排队等候,必须要其中一个进入并出来后,另一个才能进入。
而此时如果没有了第二重的person1==null是否为null的判断,则第一个线程创建了实例,而第二个线程获得锁后还是可以继续再创建新的实例,这就没有达到单例的目的。
public class Person1 { private static volatile Person1 person1 =null; private Person1(){ } public static Person1 getPerson1() { if(person1==null){ synchronized(Person1.class){ if(person1==null) person1=new Person1(); } } return person1; } }
4、静态内部类模式
public class SingleTon{ private SingleTon(){} private static class SingleTonHoler{ private static SingleTon INSTANCE = new SingleTon(); } public static SingleTon getInstance(){ return SingleTonHoler.INSTANCE; } }
25、代码块执行顺序
案例:
class B { public B() { super(); System.out.println("构造器B"); } { System.out.println("普通的代码块B"); } static { System.out.println("静态代码块B"); } } public class StaticDemo extends B { public StaticDemo() { // super(); System.out.println("构造器-StaticDemo"); } { System.out.println("普通的代码块-StaticDemo"); } static { System.out.println("静态代码块-StaticDemo"); } // public static void main(String[] args) { StaticDemo a = new StaticDemo(); } }
run:
静态代码块B
静态代码块-StaticDemo
普通的代码块B
构造器B
普通的代码块-StaticDemo
构造器-StaticDemo
相关术语:
1、构造代码块:(初始化代码块,非静态代码块)直接定义在类中,当调用了构造方法时,会先执行构造代码块(没有构造代码块就只执行自己)
2、普通代码块(局部代码块):通常定义在方法内结合if switch 循环去使用。
3、静态代码块:顾名思义static修饰的代码块。
简述类加载的初始化顺序?
1、静态初始化块只在类加载时执行,且优先于主方法执行,且只会执行一次,同时静态初始化块只能给静态变量赋值,不能初始化普通的成员变量。
2、可以在类加载的时候对静态的属性进行初始化
总结:
1、所有的静态代码块先执行,从顶级父类开始依次往后加载
2、在同一个构造器中,super() 优先于 构造代码块 优先于 构造器中的语句
3、当构造器中的第一条语句是this()时,构造器中构造代码块不存在了