Step 1:创建类,设计类的成员
Step 2:创建类的对象
Step 3:通过“对象.属性”或“对象.方法”的形式调用对象的结构
在创建对象时,未显式的赋给一个变量名,即为匿名对象。匿名对象只能使用一次。再 new 就不是同一个对象了。
method(new Person());
(1)在 Java 语言范畴中,将功能结构等封装到类中,通过类的实例化(对象),来调用具体的功能结构。
(2)涉及到 Java 语言与前端 HTML、后端数据库交互时,前、后端结构在 Java 层面交互时,都体现为类、对象。
堆(Heap):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
栈(Stack):通常所说的栈是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。
方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
练习:画出下列代码的内存解析图
public class Person {
String name;
int age = 1;
boolean isMale;
}
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "Tom";
p1.isMale = true;
Person p2 = new Person();
System.out.println(p2.name); // null
Person p3 = p1;
p3.age = 10;
}
}
变量按声明的位置不同可以分为成员变量和局部变量。其中,成员变量等同于类的属性。
(1)定义变量的格式:数据类型 变量名 = 变量值;
(2)先声明,后使用
(3)变量都有其对应的作用域
(1)在类中声明的位置不同:属性定义在类内方法外;局部变量定义在方法内、方法形参、代码块内、构造器形参、构造器内
(2)权限修饰符不同:属性可以使用权限修饰符指明其权限;局部变量不能使用权限修饰符
类的属性,根据其类型,都有默认初始化值
0
0.0
0
(或\u0000
)false
null
没有默认初始化值,在调用局部变量之前,一定要显式赋值。特别地,形参在调用时赋值即可。
属性(非静态):加载到堆空间
局部变量:加载到栈空间
(1)使用范围:在方法体内
(2)作用:① 结束方法;② 针对于有返回值类型的方法,使用“return 数据”的方式返回所需的数据
(3)注意:return 关键字后面不可以声明执行语句
一个类中,允许存在一个以上的同名方法,只要它们的参数个数或参数类型不同即可。
附:“两同一不同”:
同一个类、相同方法名;
参数列表不同(参数个数不同、参数类型不同);
举个栗子:
public void getSum(String s,int i){
}
public void getSum(int i,String s){
}
上面两个方法是重载,因为参数列表不同(参数顺序不同)。
// 方法1
public void getSum(int i,int j){
System.out.println(i + j);
}
// 方法2
public int getSum(int i,int j){
return 0;
}
// 方法3
public void getSum(int m,int n){
}
// 方法4
private void getSum(int i,int j){
}
方法 1 和方法 2 不是重载,因为参数列表相同(与返回类型无关)。
方法 1 和方法 3 也不是重载,因为参数列表相同(与参数名无关)。
方法 1 和方法 4 也不是重载,因为参数列表相同(与方法的权限无关)。
跟方法的权限修饰符、返回值类型、形参变量名、方法体都没有关系。
public class OverLoadTest {
public static void main(String[] args) {
OverLoadTest overLoadTest = new OverLoadTest();
overLoadTest.getSum(1,2); // 这里进行了自动类型提升
}
// public void getSum(int num1,int num2){
// System.out.println("你调用了第一个方法");
// }
public void getSum(double num1,double num2){
System.out.println("你调用了第二个方法");
}
}
注意:上面代码中,两个
getSum
方法是重载。注释掉第一个方法后,在调用方法时,虽然传入的参数是两个 int 型的数,但它们会进行自动类型提升,从而调用第二个方法。
JDK 5.0 新增的特性,其作用相当于数组
(1)格式:数据类型 ... 变量名
(2)当调用具有可变个数形参的方法时,传入的参数个数可以是:0 个,1 个,2 个……
(3)具有可变个数形参的方法与本类中方法名相同、形参不同的方法之间构成重载
(4)具有可变个数形参的方法与本类中方法名相同、形参为相同类型数组的方法不构成重载
(5)可变个数形参作为参数时,必须声明在参数列表的末尾
(6)方法中最多只能声明一个可变形参
(7)具有可变个数形参的方法与形参为数组的方法的遍历方法相同,赋值方法不同
说明:
(3)下面的两个方法构成重载
public void show(String str){ System.out.println(str); } public void show(String ... strs){ for (int i = 0; i < strs.length; i++) { System.out.print(strs[i] + " "); } }
(4)下面代码相当于定义了一个方法,报错
(5)可变个数形参必须声明在参数列表的末尾
(6)方法中最多只能声明一个可变个数形参
(7)
public class ChangedArgsTest4 { public static void main(String[] args) { ChangedArgsTest4 c = new ChangedArgsTest4(); // 具有可变个数形参的方法可以直接在形参列表中赋值 // 而形参为数组的方法的赋值必须遵循数组的赋值方法,如下 c.show(new String[]{"hello","my","name","is","hstar"}); } // 可变形个数参具有和数组一样的属性,如 length // public void show(String ... strs){ // for (int i = 0; i < strs.length; i++) { // System.out.print(strs[i] + " "); // } // } // 和上面具有可变个数形参的方法等效 public void show(String[] str){ for (int i = 0; i < str.length; i++) { System.out.print(str[i] + " "); } } }
如果变量是基本数据类型,此时赋值的是变量所保存的数据值
如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值
附:
引用数据类型变量存储的,不仅包含地址,还包括数据类型。
如下,arr1 和 arr2 存储的不仅包含地址还包含数据类型,两者数据类型不同,自然不能赋值。
纵观所有的高级编程语言,方法参数的传递机制可以分为三种:按名调用、按值调用和按引用调用。
附:按值调用(call by value)和值传递(pass by value)说的是一个事,其他两种以此类推。这里习惯用值传递这种称呼。
既然在 C++ 中有值传递和引用传递,那么就有人猜了在 Java 也是这样的,基本数据类型是值传递,引用数据类型是引用传递。乍一听感觉很有道理,但这个猜测是不正确的,因为 Java 中只有值传递。
Java 中的变量按数据类型分为基本数据类型和引用数据类型,现在就分别举例说明这两种变量的参数传递方式都是值传递。
public class ParamTest2 {
public static void main(String[] args) {
int num1 = 3;
int num2 = 5;
System.out.println("交换前:num1 = " + num1 + ", num2 = " + num2);
swap(num1,num2);
System.out.println("交换后:num1 = " + num1 + ", num2 = " + num2);
}
public static void swap(int x, int y){
int temp = x;
x = y;
y = temp;
}
}
上述程序的初衷是交换两个数的值,但结果并不如人意,没有实现交换,可以画一张内存中变量加载的图来解释。
由上图可知,在调用 swap 方法时,是将实参复制了一份传入到方法中,这时形参与实参是相互独立的。因此方法执行时交换的是形参的值,而不是实参的值。
public class ParamTest3 {
public static void main(String[] args) {
Student student1 = new Student("小明");
Student student2 = new Student("小美");
System.out.println("交换前:student1 = " + student1.getName());
System.out.println("交换前:student2 = " + student2.getName());
System.out.println("********我是一条可爱的分割线********");
swap(student1,student2);
System.out.println("********我是一条可爱的分割线********");
System.out.println("交换后:student1 = " + student1.getName());
System.out.println("交换后:student2 = " + student2.getName());
}
public static void swap(Student s1, Student s2){
Student temp = s1;
s1 = s2;
s2 = temp;
System.out.println("swap方法中:s1 = " + s1.getName());
System.out.println("swap方法中:s2 = " + s2.getName());
}
}
class Student{
private String name;
public String getName() {
return name;
}
public Student(String name) {
this.name = name;
}
}
上述代码在内存中加载的示意图如下所示:
如果是引用传递,则调用 swap 方法后,student1 与 student2 指向将发生改变,但事实上并未改变。由上图可知,在调用方法时,是将实参的地址值复制了一份传入到方法中,这时形参与实参是相互独立的,因此修改形参并不会影响实参。
public class TransferTest3 {
public static void main(String args[]) {
TransferTest3 test = new TransferTest3();
test.first();
}
public void first() {
int i = 5;
Value v = new Value();
v.i = 25;
second(v, i);
System.out.println(v.i);
}
public void second(Value v, int i) {
i = 0;
v.i = 20;
Value val = new Value();
v = val;
System.out.println(v.i + " " + i);
}
}
class Value {
int i = 15;
}
内存解析图:
在实际问题中,我们往往需要给属性赋值加入额外的限制条件,这个条件不能在属性声明时体现,只能通过方法进行限制条件的添加。同时,我们需要避免用户再使用“对象.属性”的方式对属性进行赋值,则需要将属性声明为私有的。
修饰符 | 类内部 | 同一个包 | 不同包的子类 | 同一个工程 |
---|---|---|---|---|
private | √ | |||
(缺省) | √ | √ | ||
protect | √ | √ | √ | |
public | √ | √ | √ | √ |
附:
关于权限修饰符
权限大小(由小到大):private、缺省、protected、public
4 种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类
修饰类的话,只能使用:public、缺省
关于类的命名:
- 在 Java 中,同一包下不能有同名的类,但在不同的包下可以重名
如:
Student student = new Student();
Teacher teacher = new Teacher("Tom",35);
(1)定义构造器的格式:权限修饰符 类名(形参列表){}
(2)如果没有显式的定义类的构造器,则系统会提供一个默认的无参构造器
(3)如果在一个类中显式的定义了构造器,则系统不再提供默认的无参构造器
(4)一个类中至少会有一个构造器
(5)同一个类中定义的多个构造器,彼此构成重载
(6)抽象类中有构造器,接口中无构造器
附:需要指出的是,构造器和方法还是有区别的。构造器用于创建对象,而方法(非静态)是由对象调用以实现某些功能。
顺序如下:
(1)默认初始化
(2)显式初始化
(3)构造器中赋值
(4)通过"对象.方法"或"对象.属性"的方式进行赋值
附:
- 前三个只执行一次,(4)可以根据需要执行多次
- 赋值结果与赋值顺序相反,从后往前看
JavaBean 是一种用 Java 语言写的可重用组件,符合如下要求:
附:每个 JavaBean 都对应数据库中的一张表,而 JavaBean 中的属性与数据库表中的字段对应。
(1)this 可以用来修饰:属性、方法、构造器
(2)this 修饰属性和方法可以理解为当前对象或当前正在创建的对象:
(3)this 调用构造器
附:
- 一个类中有 n 个构造器,其中的 n - 1 个构造器都调用了本类中的其他构造器,剩下的一个构造器不能在调用本类中的构造器,否则这种调用关系会“成环”,引发悲剧后果
- 因为"this(参数列表)"必须写在首行,所以只能使用一次该声明(联系可变个数的形参只能定义一个)
(1)使用 package 关键字声明类或接口所属的包,声明在源文件的首行
(2)包属于标识符,遵循标识符的命名规则、规范、“见名知意”
(3)每"."一次,就代表一层文件目录
(4)同一包下,不能命名同名的类、接口;不同的包下,可以命名同名的类、接口
(1)声明在包的声明和类的声明之间
(2)在源文件中显式的使用 import 结构导入指定包下的类、接口
(3)如果需要导入多个结构,则可以并列写出(多行)
(4)可以使用"xxx.*"的方式,导入 xxx 包下的所有结构
(5)使用"xxx.*"的方式后,可以调用 xxx 包下的所有结构。但如果想使用 xxx 子包下的结构,需要显式导入子包下的结构
(6)如果使用的类、接口是 java.lang 包下定义的,则可以省略 import 结构
(7)如果使用的类、接口是本包中定义的,则可以省略 import 结构
(8)如果在源文件中使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示
(9)import static:导入指定类或接口中的静态结构(属性或方法)
package com.hstar.oopexer8;
import static java.lang.System.out; // 导入静态结构
public class BoyGirlTest {
public static void main(String[] args) {
out.println("hello");
}
}
(1)减少了代码的冗余,提高了代码的复用性
(2)便于功能的扩展
(3)多态使用的前提
class A extends B{}
其中,A 称为子类、派生类、subclass,B 称为超类、基类、superclass
证明子类继承父类后,可以获取父类 private 的属性和方法:
package com.hstar.oop.extend.prove; public class Person { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void study(){ System.out.println("刻苦学习……"); gaming(); } private void gaming(){ System.out.println("放松娱乐来一把……"); } }
package com.hstar.oop.extend.prove; public class Student extends Person{ }
package com.hstar.oop.extend.prove; public class Test { public static void main(String[] args) { Student student = new Student(); // 证明继承了私有属性 student.setName("南宫辉星"); System.out.println(student.getName()); // 证明继承了私有方法 student.study(); } }
(1)一个类可以被多个子类继承
(2)Java 中类是单继承的,即一个类只能有一个(直接)父类
(3)子父类是一种相对概念。子类直接继承的父类称为直接父类,间接继承的父类称为间接父类
(4)子类继承父类后,就获取了直接父类以及所有间接父类中声明的属性和方法
(5)如果没有显式的声明一个类的父类,则此类继承于 java.lang.Object 类。Java 中所有的类(除 java.lang.Object 类外)都直接或间接的继承于 java.lang.Object 类。意味着,所有的 Java 类都具 有 java.lang.Object 类声明的功能。
子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作。
重写以后,当创建子类对象以后,通过子类对象调用父类中同名同参数的方法时,实际执行的是子类重写父类的方法。(这里不是多态的应用)
方法的声明:权限修饰符 返回值类型 方法名(形参列表) throws 异常类型{ }
约定:子类中的叫做重写的方法,父类中的叫做被重写的方法。
(1)方法名和形参列表:
(2)权限修饰符:
子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
特别地,子类不能重写父类中声明为 private 的方法
(3)返回值类型:
(4)异常类型:
附:子类和父类中同名同参数的方法都声明为非 static 的,是重写;声明为 static 的不是重写。
super 关键字可以调用:属性、方法、构造器
(1)调用属性和方法
可以在子类的构造器中,通过"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。通常情况下,习惯省略"super."
① 特别地,当子类和父类中定义了同名的属性时,如果想在子类中调用父类中声明的同名属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性。
② 特别地,当子类重写了父类中的方法后,如果想在子类中调用父类中被重写的方法,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。
(2)调用构造器
"super(形参列表)"必须声明在构造器的首行
可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定构造器
在类的构造器中,"this(形参列表)"和"super(形参列表)"只能二选一,不能同时出现
在构造器的首行,如果没有显式的声明"this(形参列表)“或"super(形参列表)”,则默认使用"super()"调用父类的空参构造器
在类的多个构造器中,至少有一个类的构造器中使用了"super(形参列表)",调用父类中的构造器
附:
- 一种极端情况:类中有 n 个 构造器,且其中 n-1 个构造器使用了 this 关键字调用其他的构造器,这时剩下的一个构造器不能再使用 this 调用其他构造器,所以它要么默认调用父类的空参构造器,要么调用父类指定的构造器。
- 如果父类中声明了有参的构造器,而没有声明无参构造器,则父类中没有无参构造器。此时,如果子类中没有显式声明构造器,则子类中有一个默认无参构造器,而该默认无参构造器会调用父类的无参构造器,所以此时程序报错。
(1)从结果上看:子类继承父类后,就获取了父类中声明的属性或方法。创建子类的对象,就会在堆空间中加载所有父类中声明的属性。
(2)从过程上看:当通过子类的构造器创建对象时,一定会直接或间接的调用父类的构造器,进而调用父类的父类的构造器,直到调用了 java.lang.Object 类中的空参构造器为止。正因为加载过所有父类的结构,所以内存中会有父类的结构,子类对象才可以调用。
附:
- 虽然在创建子类对象时调用了父类的构造器,但自始至终就创建过一个对象,即为 new 的子类对象。
- 为什么子类继承父类后,可以拥有父类的属性?
- 因为子类在通过构造器创建对象时,会直接或间接的调用父类的构造器,会加载父类的结构。
子类对象实例化图示:
对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
(1)多态性的使用前提:① 子类继承父类;② 方法的重写;③ 父类引用指向子类对象。
(2)有了对象的多态性后,在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子 类重写父类的方法。其中,调用的方法称为虚拟方法。上述可以总结为:编译看左边,运行看右 边。
(3)多态性只适用于方法,不适用于属性(属性的编译、运行都看左边)。
若子类重写了父类方法,就意味着子类中定义的方法彻底覆盖了父类中的同名方法,系统不可能把父类中的方法转移到子类中:编译看左边,运行看右边。
对于实例变量,即使子类中定义了与父类中完全相同的实例变量,子类中的实例变量也不会覆盖父类中定义的实例变量:编译、运行都看左边。
举个栗子:
public class Base {
int num = 10;
public void display(){
System.out.println(this.num);
}
}
public class Sub extends Base{
int num = 20;
public void display(){
System.out.println(this.num);
}
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
System.out.println(s.num); // 20
s.display(); // 20
System.out.println("********我是一条可爱的分割线********");
Base b = s;
System.out.println(b == s); // true
System.out.println(b.num); // 10(多态不适用于属性)
b.display(); // 20
}
}
附:
如果子类没有重写父类的方法,使用多态就没有意义了。
虚拟方法调用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法。在调用虚拟方法时,编译看左,运行看右。
问题:怎样才能执行父类的方法?答:父类设计出来的目的,就是为了实现继承,最终实现多态,可能实际中并不需要执行父类的方法。如果确实需要执行,就创建父类的对象。
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
静态绑定:在方法调用之前,或者说在编译期,编译器就已经确定了所要调用的方法。
动态绑定:只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法。
重载是静态绑定,重写(使用多态)是动态绑定。
- Bruce Eckel 说:“不要犯傻,如果它不是晚绑定,它就不是多态。”
- 重载可以是子类重载父类的方法,但在方法调用时是静态绑定,因此重载不是多态。(为什么呢?因为子类在继承父类时,继承了方法,则可以重载该方法。严谨来说,重载发生在一个类中,即使是子类中)
看源码得知:println 方法调用了 Object 类的 toString 方法,由于这里使用了多态,所以最终调用的是 Student 类重写的方法。
println 方法:
(1)如果传入的参数调用了 toString 方法,则直接调用该参数的 toString 方法。如果没有重写 toString 方法,就调用子类从 Object 类中继承过来的;如果重写了,调用子类重写。
(2)如果传入的参数没有调用 toString 方法,则会调用 println 中的 toString 方法,流程是:先将传入的对象向上转型为 Object 类型的,然后实现了父类引用在调用虚拟方法时,调用的是子类重写后的方法。
可以想象:如果不使用多态,println 方法就需要为每一个对象写一次。而有了多态,不论传入什么类型的对象,都向上转型为 Object 类型,然后通过动态绑定实现调用子类重写的方法。
有了对象的多态性后,内存中实际上加载了子类特有的属性和方法,但由于变量声明为父类类型,导致编译时只能调用父类中声明的属性和方法,子类中特有的属性和方法不能调用。
为了调用子类中特有的属性和方法,需要使用向下转型,即使用强制类型转换符,将父类引用转换为对应的子类对象。
在进行向下转型时,可能会出现 ClassCastException 的异常。为了避免出现该异常,可以在向下转型之前使用 instanceof 关键字。
a instanceof A
:判断对象 a 是否为类 A 的实例。如果是,返回 true;如果不是,返回 false。
类 B 是类 A 的父类:
如果
a instanceof A
返回 true,则a instanceof B
也返回 true。
多态的主体内容已完结,关于其他 ↓ ↓ ↓
package com.hstar.oop.polymorphism.exercise4;
public class Test {
public static void main(String[] args) {
Base base = new Sub();
base.add(1,2,3); // sub_1
Sub sub = (Sub) base;
sub.add(1,2,3); // sub_2,这里是重载,确定参数形参的优先级高于可变个数形参
}
}
class Base{
public void add(int a, int ... arr){
System.out.println("base");
}
}
class Sub extends Base{
public void add(int a, int[] arr){ // 可变个数形参和数组等价,这里是方法的重写
System.out.println("sub_1");
}
public void add(int a, int b, int c){
System.out.println("sub_2");
}
}
总结:可变个数形参和数组等价。
如下,Student 类只是继承了 Person 类,而并未重写 eat 方法。所以父类引用 p 调用的是子类继承过来的 eat 方法。
public class Person {
public void eat(){
System.out.println("吃饭");
}
}
class Student extends Person{
}
public class PersonTest {
public static void main(String[] args) {
Person p = new Student();
System.out.println(p.getClass()); // class com.hstar.oopexer7.Student
}
}
多态是编译时行为还是运行时行为?如何证明?
package com.atguigu.test;
import java.util.Random;
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class InterviewTest {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}
public static void main(String[] args) {
int key = new Random().nextInt(3);
System.out.println(key);
Animal animal = getInstance(key);
animal.eat();
}
}
(1)Object 类是所有 Java 类的根父类
(2)如果在类的声明中未使用 extends 关键字指明其父类,则默认父类为 java.lang.Object 类
(3)Object 类中的功能(属性、方法)具有通用性
(4)Object 类中只声明了一个空参构造器
可以使用在基本数据类型变量和引用数据类型变量中
① 如果比较的是基本数据类型变量,则比较两个变量保存的数据是否相等(两个变量不一定类型相同)
byte b = 1;
short s = 2;
float f = 1.0f;
double d = 1.0;
System.out.println(b == f); // true(自动类型提升)
System.out.println(s == d); // false
② 如果比较的是引用数据类型变量,则比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体(必须保证符号两边的变量类型一致)
Person per = new Person();
Student stu = new Student();
System.out.println(per == stu); // 编译出错
是一个方法,而非运算符
只适用于引用数据类型
Object 类中 equals 方法的定义:
public boolean equals(Object obj) {
return (this == obj);
}
Object 类中定义的 equals 方法和运算符 == 的作用等效,比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体
如 String、Date、File、包装类等都重写了 Object 类中的 equals 方法。重写后,比较的不再是两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同
通常情况下,我们自定义的类中如果使用 equals 方法,愿景也是比较两个对象的“实体内容”是否相同。那么,就需要重写 equals 方法。
重写的原则:比较两个对象的“实体内容”是否相同
重写的思路:
传参时,进行了向上转型,转换为 Object 类型;
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj instanceof Student){
Student stu = (Student) obj;
// return this.id == stu.id && this.name == stu.name;
return this.id == stu.id && this.name.equals(stu.name);
}
return false;
}
附:
总结:在重写 equals 方法时,如果需要比较的属性是引用类型的变量,则在判断两个属性的内容是否相同时,需要考虑此处调用的 equals 方法是否需要重写。但如果是 String 类型的变量,可以直接使用 equals 方法,因为该方法已经被重写;而且,String 也可以直接使用 == 。
(1)输出一个对象的引用时,实际上调用了当前对象的 toString 方法
(2)Object 类中 toString 方法的定义:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
(3)如String、Date、File、包装类等都重写了 Object 类中的 toString 方法,使得在调用对象的 toString 方法时,返回的是“实体内容”
引用数据类型包括类、接口和数组。类和数组的根父类是 Object 类;而接口相对独立
package com.hstar.commonclass.objectclass.subclass;
import org.junit.Test;
public class SubClass {
@Test
public void test(){
int[] arr = new int[]{1,2,3};
System.out.println(arr instanceof Object); // true
System.out.println(arr.getClass()); // class [I
System.out.println(arr.getClass().getSuperclass()); // class java.lang.Object
}
}
总结:数组也作为 Object 类的子类,可以调用 Object 类中声明的方法。
为了使基本数据类型的变量具有类的特征。
(1)构造器1,参数为相应的基本数据类型;
(2)构造器2,字符串参数
对于构造器 1,所有转换都类似
对于构造器 2,不同的包装类,有一些差别,具体如下:
数值型:如 int、float 等,在使用构造器 2 时,字符串里面的内容应该严格是该类型的数值,否则报错,如Integer integer = new Integer("123abc");
→ NumberFormatException。
布尔型:在忽略大小写的情况下,如果字符串中不是“true”,结果就是 false。
Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("TrUe");
Boolean b3 = new Boolean("true123");
System.out.println(b1); // true
System.out.println(b2); // true(忽略大小写)
System.out.println(b3); // false(不是true就是false)
调用包装类 Xxx 的 "XxxValue()"方法
Integer integer = new Integer(10);
int i = integer.intValue();
System.out.println(i);
JDK 5.0 新特性
自动装箱:基本数据类型 → 包装类
自动拆箱:包装类 → 基本数据类型
Integer integer = 10; // 自动装箱
int num = integer; // 自动拆箱
方式一:连接运算
int num1 = 10;
String str1 = num1 + "";
System.out.println(str1);
方式二:调用 String 重载的 valueOf 方法
String str2 = String.valueOf(10); // 基本数据类型
Integer integer = 11;
String str3 = String.valueOf(integer); // 包装类
System.out.println(str2);
System.out.println(str3);
调用包装类的 parseXxx 方法
String str4 = "111";
int i = Integer.parseInt(str4);
System.out.println(i);
String str5 = "TruE"; // 不区分大小写的 true,其结果都是“true”
boolean b = Boolean.parseBoolean(str5);
System.out.println(b);
总结:
public class Interview {
@Test
public void test1(){
Object o1 = true ? new Integer(1) : new Double(2.0);// 三元运算符在编译时会进行自动类型提升
System.out.println(o1); // 1.0
}
@Test
public void test2(){
Object o2;
if(true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2); // 不存在自动类型提升
}
@Test // 练习3
public void test3(){
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j); // false
Integer m = 1;
Integer n = 1;
System.out.println(m == n); // true
Integer x = 128;
Integer y = 128;
System.out.println(x == y); // false
}
}
对于练习3:
Integer 内部定义了 IntegerCache 结构,IntegerCache 中定义了 Integer[],该数组中保存了 -128 ~ 127 范围内的整数。如果使用自动装箱的方式,给 Integer 赋值的范围在 -128 ~ 127 范围内时,可以直接使用数组中的元素,不用再去 new 了。目的:提高效率。
static 可以用来修饰:属性、方法、代码块、内部类
static 修饰的属性称为静态变量或类变量;没用 static 修饰的属性称为实例变量。
附:变量按声明的位置可以分为成员变量和局部变量:
成员变量:在方法体外,类体内声明的变量
局部变量:在方法体内部声明的变量
注意:两者在初始化值方面的异同:
同:都有声明周期
异:局部变量除形参外,需显式初始化
static 修饰的方法称为静态方法。关于静态方法:
随着类的加载而加载,可以通过"类.静态方法"的方式进行调用
关于静态方法与非静态方法的调用:
静态方法 | 非静态方法 | |
---|---|---|
类 | 可以调用 | 不可以调用 |
对象 | 可以调用 | 可以调用 |
静态方法中只能调用静态的方法或属性;非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性。
附:静态方法中也可以调用非静态方法,只不过需要通过“对象.方法”的形式调用。
public class StaticTest { public static void main(String[] args) { StaticTest staticTest = new StaticTest(); staticTest.show(); // 利用对象调用非静态方法 } public void show(){ System.out.println("我是一个非静态方法"); } }
static 不能修饰构造器
static 修饰的是与对象无关的内容,而构造器却要生成对象。另外。在时间维度上,static 修饰的内容随着类的加载而加载,而对象的创建要晚于类的加载。
在静态方法中,不能使用 this、super 关键字;在静态方法中,不能访问非静态的属性和方法
由于静态方法是随着类的加载而加载的,这时还没有对象。在静态方法的内部无法确定非 static 的内容属于哪个对象,同样地使用 this 也是模糊不清的。
static 修饰的方法不能被重写,但可以被继承
① 证明 static 修饰的方法不能被重写
package com.hstar.oop.static_keyword;
public class StaticTest {
public static void main(String[] args) {
Base base = new Son();
base.method1(); // Base_method1。如果能被重写。此处应该体现多态
base.method2(); // Son_method2
}
}
class Base{
public static void method1(){
System.out.println("Base_method1");
}
public void method2(){
System.out.println("Base_method2");
}
}
class Son extends Base{
public static void method1(){
System.out.println("Son_method1");
}
public void method2(){
System.out.println("Son_method2");
}
}
② 证明 static 修饰的方法可以被继承
package com.hstar.oop.static_keyword;
public class StaticTest1 {
public static void main(String[] args) {
Child.method();
}
}
class Parent{
public static void method(){
System.out.println("Parent");
}
}
class Child extends Parent{
// public static void method(){
// System.out.println("Child");
// }
}
附:静态的方法可以被继承,但是不能重写。
如果父类中有一个静态的方法,子类也有一个与其方法名、参数类型、参数个数都一样的 static 方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。也就是说父类的方法和子类的方法是两个没有关系的方法,具体调用哪一个方法要看是哪个对象的引用,因而这种父子类方法不存在多态的性质。
开发中,如何确定一个属性是否要声明为 static 的?
开发中,如何确定一个方法是否要声明为 static 的?
静态变量随着类的加载而加载,加载到方法区的静态域中。
举个栗子:
package com.hstar.oop.static_keyword;
public class Student {
int id;
String name;
static String school;
}
package com.hstar.oop.static_keyword;
public class StudentTest {
public static void main(String[] args) {
Student.school = "清华大学";
Student student1 = new Student();
student1.id = 1001;
student1.name = "南宫辉星";
Student student2 = new Student();
student2.id = 1005;
student2.name = "刘银河";
student1.school = "北邮";
student2.school = "南大";
}
}
内存解析图如下:
单例设计模式,就是采取一定的方法保证在整个软件体系中,对某个类只能存在一个对象实例。单例模式有两种:饿汉式单例模式和懒汉式单例模式。
package com.hstar.oop.designpatterns.singleton;
public class SingletonTest1 {
public static void main(String[] args) {
Bank bank1 = Bank.getInstance();
Bank bank2 = Bank.getInstance();
System.out.println(bank1 == bank2); // true
}
}
class Bank{
// 1. 私有化类的构造器
private Bank(){
}
// 2. 在类的内部创建对象
// 4. 要求此对象也必须声明为静态的
private static Bank instance = new Bank();
// 3. 提供公共的静态方法,返回类的对象,用于其他类使用本类对象
public static Bank getInstance(){
return instance;
}
}
package com.hstar.oop.designpatterns.singleton;
public class SingletonTest2 {
public static void main(String[] args) {
Order order1 = Order.getInstance();
Order order2 = Order.getInstance();
System.out.println(order1 == order2); // true
}
}
class Order{
// 1. 私有化类的构造器
private Order(){
}
// 2. 声明类的对象,不初始化
// 4. 此对象也必须声明为static的
private static Order instance = null;
// 3. 声明public、static方法,返回当前类的对象
public static Order getInstance(){
if(instance == null){
instance = new Order();
}
return instance;
}
}
饿汉式:
懒汉式:
由于单例模式只生成一个实例,减少了系统的性能开销。当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过应用启动时直接产生一个单例对象,然后永久驻留内存的方式解决。如 java.lang.Runtime。
网站的计数器,一般也是单例模式实现,否则难以同步。
应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
Application 也是单例的典型应用
Windows 的 Task Manager (任务管理器)就是很典型的单例模式
Windows 的 Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
附:
单例模式的设计思想就是只产生一个对象,下面提供一种非典型的单例模式:
package com.hstar.oop.designpatterns.singleton; // 也是一种单例模式,只是程序的可扩展性差 public class SingletonTest3 { public static void main(String[] args) { Student stu1 = Student.instance; Student stu2 = Student.instance; System.out.println(stu1 == stu2); // true // Student.instance = null; } } class Student{ private Student(){ } // 定义为静态常量,只能调用且不可修改 public static final Student instance = new Student(); }
static 的主体内容已完结,关于其他 ↓ ↓ ↓
public static void main(String[] args){ //方法体}
main 方法的使用说明:
类的成员之一。
代码块的作用:用来初始化类、对象
代码块如果有修饰的话,只能使用 static
代码块分为静态代码块和非静态代码块
内部可以定义 Java 语句
随着类的加载而加载,而且只执行一次
作用:初始化类的信息
如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
静态代码块的执行要优先于非静态代码块的执行
静态代码块内只能调用静态的属性、方法,不能调用非静态的结构
代码块主线:
① 作用:初始化类、对象
② 内容:输出语句、初始化语句。静态中只能调用静态属性、方法;非静态中可以包括静态、 非静态结构
③ 执行时间:静态 → 随类加载而执行;非静态 → 随对象创建而执行
执行次数:静态:只执行一次;非静态:创建多少个对象,执行多少次
④ 存在多个静态代码块时,按声明顺序执行;非静态同静态。静态与非静态同时存在时,先执 行静态
代码块的主体内容已完结,关于其他 ↓ ↓ ↓
① 默认初始化
② 显式初始化
③ 构造器中初始化
④ 有了对象后,可以通过"对象.属性"或"对象.方法"的方式进行赋值
⑤ 在代码块中赋值
① 默认初始化
② 显式初始化 / ⑤ 在代码块中赋值
③ 构造器中初始化
④ 有了对象后,可以通过"对象.属性"或"对象.方法"的方式进行赋值
由上可知:
- 显式初始化和代码块中赋值的先后顺序看定义的相对顺序
- 代码块的执行要先于构造器
final 可以用来修饰的结构:变量、方法、类
final 修饰的类不能被其他类继承。如 String、System、StringBuffer
因为这些类已经提供了足够的功能,不需要再继承扩充功能了。
final 修饰的方法不可以被重写。如 Object 类中的 getClass 方法
因为这个方法提供的功能够使了,不需要再重写了。
final 修饰变量后,此“变量”就变为一个常量。另外,可以用 static final 来定义一个全局常量。
final 修饰属性:可以考虑赋值的位置,显式初始化、代码块中初始化、构造器中初始化
附:如果每个对象的属性值都相同,考虑显式初始化;如果每个对象的属性值不同,考虑构造器初始化;代码块用于调用方法赋值的情况。
final 修饰局部变量:尤其是使用 final 修饰形参时,表明此形参是一个常量。当调用此方法时,给常量形参赋一个实参,一旦赋值后,就只能在方法体中使用此形参,而不能再重新赋值。
public void show(final int number){ // 在调用方法时传入实参
// number = 5; // 编译出错
System.out.println(number);
}
如果有多个构造器,只要一个构造器对 final“变量”进行了赋值,其他构造器也必须赋值。(因为 final “变量”一定要初始化,但个别构造器中未给 final “变量”赋值,这就导致调用这些构造器后,该“变量”可能会未被赋值)
final int NUM3;
// 如果有多个构造器,只要一个构造器对“常量”进行了赋值,其他构造器也必须赋值
public FinalTest(){
NUM3 = 2;
}
public FinalTest(int num){
this.NUM3 = num;
}
总结:对于属性,如果要用“final”修饰,就一定要考虑初始化。
public class Something{
public int addOne(final int x){
// return ++x; // 编译出错,因为改变了 x 的值
return x + 1; // 编译通过,因为没有改变 x 的值
}
}
public class Something {
public static void main(String[] args) {
Other o = new Other();
new Something().addOne(o);
System.out.println(o.i); // 1
}
public void addOne(final Other o){
// o = new Other(); // 编译出错
o.i++; // 编译通过。址值不变,但是地址值指向的堆空间中的属性可以变。
}
}
class Other{
public int i;
}
abstract 可以用来修饰的结构:方法、类
表示“抽象方法”,抽象方法只有方法的声明,没有方法体
包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法
因为抽象方法没有方法体,不能被调用,所以为了做到不被调用,就将类声明为抽象的,这样类就不能产生对象,也就不会调用抽象方法了。所以抽象方法所在的类一定是抽象类。
另外,抽象类中不定义抽象方法没意义。
若子类中重写了父类中的所有抽象方法后,则此子类可以实例化;若子类没有重写父类中的所有抽象方法,则此子类仍然是一个抽象类
附:
总结方法不能重写的情况
- 子类可以继承父类的 private 方法,但“看不见”,也就没办法重写
- static 方法不能重写
- final 修饰的方法不能重写
总结不能修饰构造器的关键字
- static
- abstract
在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。
数据库访问的封装
Junit 单元测试
JavaWeb 的 Servlet 中关于 doGet/doPost 方法调用
Hibernate 中模板程序
Spring 中 JDBCTemlate、HibernateTemplate 等
(1)Java 中接口使用 interface 关键字定义。而且类与接口是两个并列的结构
(2)接口中的成员:
JDK 7 及以前:只能定义全局常量和抽象方法
public static final
的,在书写时可以省略public abstract
的,同样可以是省略JDK 8 及以后:除了定义全局常量和抽象方法外,还可以定义静态方法、默认方法
① 接口中定义的静态方法,只能通过接口来调用。
② 通过实现类的对象,可以调用接口中的默认方法。如果实现类重写了接口中的默认方法,则在调用时,执行的是实现类重写的方法。(多态)
(3)接口不同于抽象类,接口中不能定义构造器,意味着接口不能实例化
(4)Java 开发中,接口通过让类去实现(implements)的方式来使用。如果实现类覆盖了接口中 的所有抽象方法,则此实现类可以实例化;如果实现类没有覆盖接口中的所有抽象方法,则此实现 类仍为一个抽象类。
类优先原则(只适用于方法):如果一个类同时继承一个类和实现一个接口(继承写在前面),且父类和接口中声明了同名同参数的方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的方法。
接口冲突:如果实现类实现了多个接口,而这些接口中的多个都定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下会报错,解决:在实现类中重写该默认方法。
如何在子类(或实现类)的方法中调用父类、接口中(被重写)的方法
class SubClass extends SuperClass implements CompareA{
public void method2(){
System.out.println("在接口的实现类中重写默认方法");
}
// 在子类(或实现类)中调用父类或接口中的方法
public void MyMethod(){
super.method3(); // 调用父类中的方法
CompareA.super.method3(); // 调用接口中的方法
}
}
(4)Java 类可以实现多个接口,这弥补了 Java 单继承的局限性
class A extends B implements C,D,E{ }
接口与接口之间可以继承,而且可以多继承。
总结:
① JDK 8及以后,接口中可以定义 abstract 方法、static 方法和 default 方法。其中,只有 default 方法可以被重写。
② 类与类之间是的继承是单继承:一个类只能有一个父类,一个类可以被多个类继承;
接口与接口之间可以是多继承:一个接口可以继承多个接口;
类与接口之间是实现:一个类可以实现多个接口,同时还可以继承其他类。
代理模式是 Java 开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。
安全代理:屏蔽对真实角色的直接访问。
远程代理:通过代理类处理远程方法调用(RMI)
延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象。比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100 MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用 proxy 来进行大图片的打开。
静态代理(静态定义代理类)
动态代理(动态生成代理类) JDK 自带的动态代理,需要反射等知识
接口的主体内容已完结,关于其他 ↓ ↓ ↓
package com.hstar.oop.interfacetest.java8;
interface A{
int x = 0;
}
class B{
int x = 1;
}
public class C extends B implements A{
public void px(){
// System.out.println(x); // Reference to 'x' is ambiguous, both 'B.x' and 'A.x' match
System.out.println("父类中的:x = " + super.x);
System.out.println("接口中的:x = " + A.x);
}
public static void main(String[] args) {
new C().px();
}
}
附:属性不适用于类优先原则。
编译不通过:ball 是静态常量。
Java 中允许将一个类 A 声明在另一个类 B 中,则类 A 就是内部类,类 B 称为外部类
成员内部类:静态成员内部类、非静态成员内部类
局部内部类:方法内、代码块内、构造器内
成员内部类和局部内部类在编译后,都会生成字节码文件,格式:
成员内部类:外部类$内部类名.class
局部内部类:外部类$数字内部类名.class
package com.hstar.oop.innerclass.inner1;
public class Person {
// 静态成员内部类
static class Eye{
public Eye(){
}
}
// 非静态成员内部类
class Nose{
public Nose(){
}
}
}
package com.hstar.oop.innerclass.inner1;
public class InnerClassTest {
public static void main(String[] args) {
// 创建Eye的实例(静态成员内部类)
Person.Eye eye = new Person.Eye();
// 创建Nose的实例(非静态成员内部类)
Person person = new Person();
Person.Nose nose = person.new Nose();
}
}
package com.hstar.oop.innerclass.inner1;
public class Person {
String name = "辉星";
public void eat(){
System.out.println("吃饭");
}
// 非静态成员内部类2
class Nose{
String name = "鼻子";
public Nose(){
}
public void method(){
System.out.println("鼻子闻到了远处飘来的花香");
Person.this.eat(); // 调用外部类的非静态方法
}
// 如何在成员内部类中区分调用外部类的结构
public void method1(String name){
System.out.println(name); // 打印形参变量
System.out.println(this.name); // 打印内部类的成员变量
System.out.println(Person.this.name); // 打印外部类的成员变量
}
}
}
开发中局部内部类的使用:返回接口实现类的对象
package com.hstar.oop.innerclass.inner1;
public class InnerClassTest1 {
// 返回一个实现了Comparable接口的类的对象
public Comparable getComparable(){
// 创建一个实现了Comparable接口的类:局部内部类
class MyComparable implements Comparable{
@Override
public int compareTo(Object o) {
return 0;
}
}
return new MyComparable();
// 匿名实现类的匿名对象
// return new Comparable() {
// @Override
// public int compareTo(Object o) {
// return 0;
// }
// };
}
}
在局部内部类的方法中,如果想要调用局部内部类所在的方法中的局部变量,需要将该局部变量定义为 final 的。
public class InnerClassTest2 {
public void method(){
final int num = 10;
class AA{
public void show(){
// num = 20;
System.out.println(num);
}
}
}
}
JDK中的内部类:
Integer --> IntegerCache
Thread --> State