对象操作与内存
网上关于如何理解对象的文章很多,这里就不在多做阐述了。
这里重点看一下对象创建和使用时,内存里是怎么做的。
public class Student {
public String name;
public int age;
public void say() {
System.out.println("name = " + name + ", age= " + age);
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.name = "张三";
s.age = 21;
s.say(); // name = 张三, age= 21
}
}
运行上面的代码,首先java虚拟机(JVM)会把Test.java编译成字节码文件Test.class,并把Test.class加载进内存中的方法区。
然后JVM就会调用主方法(main()方法),main()方法进栈。紧接着就是创建Student类型的引用s,如果此时字节码文件Student.class已经编译完成,JVM就会直接把它加载进内存,反之,JVM会先把Student.java编译成字节码文件Student.class,然后再把Student.class加载进内存。
接下来是创建Student对象。JVM会在堆内存中给Student对象分配一块内存,并且给Student对象的各个成员变量赋初始值,完成Student对象的初始化之后,JVM会把给Student对象分配的内存的地址值赋值给指向Student对象的引用s。然后就可以根据引用s所记录的地址值在堆内存中找到Student对象,从而给Student对象的各成员变量赋值。
之后JVM调用Student对象的say()方法,say()方法进栈,say()方法执行完成之后弹栈。
至此main()方法中的代码全部执行完成,main()方法弹栈。此时堆内存中的Student对象没有任何引用指向它,垃圾回收机制(Garbage Collection)会不定时的回收它。
具体可以参考下图:
import关键字和package关键字
java中类的全称是包名加上类名。package关键字可以表示某个类所在包的包名。
package org.hu.test.entity; // entity包下的Person
public class Person {
public void say() {
System.out.println("person in entity");
}
}
上面的Person类,它的全称是org.hu.test.entity.Person。打个比方,package名称就像是我们的姓氏,而class名称就像是我们的名字。
所以如果存在相同名字的类,就可以用包名区分开来。
package org.hu.test.controller; // controller包下的Person
public class Person {
public void say() {
System.out.println("person in controller");
}
}
package org.hu.test.controller;
public class Test {
public static void main(String[] args) {
org.hu.test.entity.Person p = new org.hu.test.entity.Person(); // 使用entity包下的Person
p.say(); // person in entity
}
}
但是从上面创建Person对象的代码中可以体会到,这样写代码实在太麻烦了。于是import关键字出现了,只需要在java文件开头部分导入相应的类的全名一次,之后就只需要写类名就可以引用该类。
import可以只导入一个包里的一个类文件,也可以用“*”导入一个包中的所有类文件。
package org.hu.test.controller;
import org.hu.test.entity.Person; // 导入一个类文件
//import org.hu.test.entity.*; // 导入包下所有类文件
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.say(); // person
}
}
但是一般情况下,我们不会用到一个包内所有的类文件,而且导入所有类文件反而增加了内存的开销。
权限修饰符
本类 | 同一包下 | 不同包下(子类) | 不同包下(无关类) | |
private | y | |||
默认 | y | y | ||
protected | y | y | y | |
public | y | y | y | y |
四种权限修饰符的作用域如上所示。
但是需要明确的是, protected修饰符作用的“不同包下的子类”,指的是用该修饰符修饰的成员只能在子类的内部使用。具体看这样一个例子:
package org.hu.test.entity; // entity包下
public class Person {
public String name;
protected String city = "四川";
}
package org.hu.test.controller; // controlller包下
import org.hu.test.entity.Person;
public class Student extends Person {
public void getcity() {
System.out.println("city: " + city);
}
}
package org.hu.test.controller; // controller包下
public class Test {
public static void main(String[] args) {
Student s = new Student();
// s.city; // 错误, 在不同包下,用protected修饰的成员只能在子类中访问
s.getcity(); // city: 四川
}
}
tip:如果一个类中全部都是静态方法,那么就可以把该类的构造方法声明为private,不让别的类用该类来创建对象。
public class Arrays { // 工具类 Arrays
private Arrays() {}......}
public class Collections { // 工具类 Collections
private Collections() {
.......}
成员变量与局部变量
- 类中位置不同
- 成员变量:类中方法外
- 局部变量:方法体中或方法签名中
- 内存中位置不同
- 成员变量:堆内存
- 局部变量:栈内存
- 生命周期不同
- 成员变量:随着对象创建而存在,随着对象消失而消失
- 局部变量:随着方法调用而存在,随着方法调用结束而消失
- 初始化值不同
- 成员变量:有默认初始值
- 局部变量:无默认初始值,必须定义赋值才能使用
tip:局部变量名可以与成员变量名一致,方法中使用采用“就近原则”。
public class Student {
public String name;
public int age;
public void say() {
int age = 18;
String name = "李四";
System.out.println("name = " + name + ", age= " + age);
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.name = "张三";
s.age = 21;
s.say(); // name = 李四, age= 18
}
}
代码块
局部代码块:方法中。
构造代码块:类中方法外,优先于构造方法执行。
静态代码块:类中方法外,随着类的加载而加载, 优先于主方法执行。
public class Person {
public String name;
public int age;
static {
System.out.println("Person静态代码块");
}
{
System.out.println("Person构造代码块");
}
public Person() {
System.out.println("Person空参构造");
}
}
public class Student extends Person {
static {
System.out.println("Student静态代码块");
}
{
System.out.println("Student构造代码块");
}
public Student() {
super();
System.out.println("Student空参构造");
}
}
public class Test {
public static void main(String[] args) {
new Student();
/*
Person静态代码块
Student静态代码块
Person构造代码块
Person空参构造
Student构造代码块
Student空参构造
*/
}
}
this关键字
this代表当前对象的引用。在对象创建的时候,this就被赋值当前对象的地址值。
public class Person {
{
System.out.println("this: " + this);
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person(); // p: org.hu.test.entity.Person@15db9742
System.out.println("p: " + p); // this: org.hu.test.entity.Person@15db9742
}
}
构造方法的重载
- 如果某类中不存在构造方法,系统默认给此类提供空参构造函数
- 如果某类中存在构造方法,系统不会再给此类提供空参构造方法
static关键字
类中用static关键字修饰的成员不再是某个对象的私有属性,而是所有对象共有属性,也可以称之为类成员。
用static修饰的成员的特点:
- 随着类的加载而加载(更准确的说是随着字节码文件的加载而加载)
- 优先于对象而存在
- 可以直接通过类名调用
tip:静态方法不可以访问非静态成员变量,静态只能访问静态。
public class Person {
public String name;
public static String city;
public void say() {
System.out.println("name= " + name + ", city=" + city);
}
}
public class test {
public static void main(String[] args) {
Person.city = "四川";
Person p = new Person();
p.name = "李四";
p.say(); // name= 李四, city=四川
p.city = "上海";
p.say(); // name= 李四, city=上海
}
}
用static关键字修饰的成员在内存中的位置是方法区中的共享区,普通方法则在非共享区。可以参考下图:
二者区别可以用解压缩文件和压缩文件来形容。虽然它们都是随着类的加载而加载,但是前者一开始就是解压缩的状态,所以可以直接用类名来调用,而后者则需要创建对象来解压缩才能使用。
继承
java只支持单继承,但是可以多层继承。
如果类之间可以多继承,那么在子类中就无法区分从多个父类中继承来的同名成员。
public class Person {
private String name = "person";
public String city = "四川";
public Integer age = 21;
}
public class Student extends Person {
public String city = "上海";
public String sex = "man";
public void getPersonInfo() {
System.out.print("Person city: " + super.city);
System.out.println(" ,age: " + this.age);
// System.out.println("sex: " + super.sex); // 错误 : super只能访问父类成员
}
public void getStudentInfo() {
System.out.print("Student city: " + this.city);
System.out.println(" , sex: " + sex); // 直接写成员变量名也可以,系统会自动加上this.前缀
// System.out.println("name: " + super.name); // 错误: 子类不能继承父类的非私有成员
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.getStudentInfo(); // Student city: 上海 , sex: man
s.getPersonInfo(); // Person city: 四川 ,age: 21
}
}
从上面代码可以看出:
- 子类只能继承父类非私有的成员。
- 子父类成员同名,采用就近原则--子类成员覆盖父类成员,但是可以用this和super区分子父类成员。
- this和super的区别:
- this既可以访问子类中的成员变量,也可以访问父类中的成员变量。
- super 只能访问父类中的成员变量。
public class Person {
public String name;
public Person() {
System.out.print("Person空参构造方法");
}
}
public class Student extends Person {
public Student() {
System.out.println(", Student空参构造方法");
}
public Student(String name) {
this.name = name;
System.out.println(", Studnet有参构造方法");
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student(); // Person构造方法, Student构造方法
Student s2 = new Student("张三"); // Person空参构造方法, Studnet有参构造方法
}
}
通过上面可以看到,子类的每一个构造方法中,如果不手动加上super(),系统都会默认帮我们加上。
如果我们自定义一个类而且没有继承任何类呢?那么这个类也还是会调用Object类的空参构造,因为在Java中所有的类都继承于Object类,但不用在声明一个类时显示的extends Object。
但是通过之前所说的构造方法的重载,我们又可得知:
如果某类中存在构造方法,系统不会再给此类提供空参构造方法
所以如果父类中只有有参构造方法,子类的构造方法中必须调用父类的有参构造方法(或者用this调用本类其他构造方法)。
看下面的例子:
public class Person {
public String name;
public Person(String name) {
System.out.print("Person有参构造方法");
}
}
public class Student extends Person {
// Implicit super constructor Person() is undefined. Must explicitly invoke another constructor
/*public Student() {
System.out.println(", Student空参构造方法");
}*/
public Student() {
//super("张三"); // 错误: 一个构造方法中super()和this()不得同时出现
this("张三"); // 调用本类的其他构造方法
System.out.println(", Studnet空参构造方法");
}
public Student(String name) {
// System.out.println("Studnet空参构造方法, "); // 错误, 不可在调用父类构造方法之前写语句
super(name); // 必须调用父类的有参构造
System.out.print(", Studnet有参构造方法");
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student(); // Person有参构造方法, Studnet有参构造方法, Studnet空参构造方法
Student s2 = new Student("张三"); // Person有参构造方法, Studnet有参构造方法
}
}
总结:
- 子类构造方法必须访问父类构造方法
- f构造方法中有this()没super(),有super()没this()
- super()/this()必须放在构造方法中第一句,防止子类在构造方法中使用尚未从父类继承下来的成员变量
public class Person {
private void say() {
System.out.println("i am person"); // 子类无法继承父类的私有方法
}
public void start() {
System.out.print("person go, ");
}
public void limit() {
System.out.println("person limit");
}
}
public class Student extends Person {
public void say() {
System.out.println("i am student");
}
public void start() {
super.start(); // 可以通过super访问被子类重写的父类方法
System.out.println("student go");
}
/*void limit() {
System.out.println("student limit"); // 子类方法权限需要大于或等于父类, 才能完成对父类方法的重写
}*/
public void limit () {
System.out.println("student limit");
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
s1.say(); // i am student
s1.start(); // person go, student go
s1.limit(); // student limit
}
}
从上面的代码可以看出来,如果子父类成员方法同名,子类方法将重写父类的非私有成员方法(子类方法权限需要大于等于父类),但是我们依然可以通过super来访问被子类重写的父类方法。
final关键字
- final修饰的类:子类无法继承(如String,System)
- final修饰的方法:子类无法重写(父类中的方法需要可以让外部使用,但是又不希望被子类重写,可以用final修饰)
- final修饰的变量:1. 基本数据类型变量:又称之为常量。 2. 引用数据类型变量:final用来修饰引用数据类型变量时,和C语言中使用const关键字固定指针的用法一样,固定的是引用指向的对象的地址,但地址上存放的数据还是可以更改。
public class Person {
public String name;
public int age;
public Person() {
this.name = "李四";
this.age = 18;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
final Person p = new Person();
// p = new Person(); // 引用p不可以再指向新的对象
System.out.println(p); // Person [name=李四, age=18]
p.setName("张三");
p.setAge(21);
System.out.println(p); // Person [name=张三, age=21]
}
}
tips:final修饰的变量必须初始化。有两种方式:
- 定义时直接赋值。
- 构造函数赋值。(很少)
多态
public class Person {
public String name = "person";
public static String work = "work";
public void say() {
System.out.println("i am person");
}
public static void start() {
System.out.println("start to work");
}
}
public class Student extends Person {
public String name = "student";
public static String work = "study";
public void say() {
System.out.println("i am student");
}
public static void start() {
System.out.println("start to study");
}
}
public class Test {
public static void main(String[] args) {
Person p = new Student();
System.out.println(p.name); // person
System.out.println(p.work); // work
p.say(); // i am student
p.start(); // start to work
}
}
从上面的代码中可以看出来,当父类和子类中成员的定义完全一致,使用父类引用p指向子类Student对象,分别调用子类对象的静态成员变量、静态成员方法、非静态成员变量、非静态成员方法,除非静态成员方法,调用的其他成员都是父类的成员。
静态成员是随着类的加载而加载,p.work和p.start()其实就可以替换成Person.work和Person.start(),所以调用结果是父类成员也就不奇怪了。
p.name为什么输出也是父类的成员变量呢?其实在堆内存中,一个对象的内部是分为两块区域的,一块区域存放的是继承自父类的成员变量(super),另一块区域存放的才是本类的成员变量(this)。由于是父类引用调用的成员变量name,所以在内存中访问到的其实是继承自父类的成员变量。
只有父类引用调用成员方法say()的时候,才使用了子类对象中的成员方法,这就是动态绑定。
具体可以参考下图:
总结起来,要体现多态有三个前提:
- 继承
- 重写父类成员方法
- 父类引用指向子类对象
多态的好处:将父类作为参数传递,提高代码的可扩展性
多态的弊端:不能使用子类的特有功能
abstract关键字
用abstract关键字修饰的类是抽象类,用abstract关键字修饰的方法是抽象方法,abstract关键字不能用来修饰变量。
抽象方法只存在于抽象类中,但是抽象类中可以没有抽象方法。
抽象类是无法被实例化的,但是抽象类中依然存在构造方法,上面提到过:
子类构造方法必须访问父类构造方法
所以抽象类中的构造方法就是为了非抽象子类能够实例化而存在的。
关键字冲突:
-
abstract关键字和private关键字冲突,使用前者的目的是要子类去实现关键字修饰的方法,使用后者的目的是为了不让子类看到关键字修饰的成员。
-
abstract关键字和static关键字冲突,前者修饰的方法没有具体的实现,后者修饰的方法可以直接用类名调用,而用类名调用抽象方法毫无意义。
-
abstract关键字和final关键字冲突,使用前者修饰的方法是为了让子类重写,使用后者修饰的方法是为了不让子类重写。
看下面一个例子:
public abstract class Person {
public Person() {
System.out.println("Person构造方法"); // 抽象类的构造方法是为了让非抽象子类可以创建对象对象
}
public abstract void say(); // 抽象方法:只有方法签名, 没有具体的实现
public void work() {
System.out.println("person work"); // 抽象父类中的可以有普通成员方法, 子类可以正常继承使用
}
}
public class Student extends Person {
@Override
public void say() {
System.out.println("i am student"); // 非抽象子类必须重写抽象父类的抽象方法
}
// public abstract void studentsay(); // 非抽象类中不可以存在抽象方法
}
public abstract class Teacher extends Person {
// private abstract void say1(); // 错误: 关键字冲突
// public static abstract void say2(); // 错误: 关键字冲突
// private final abstract void say3(); // 错误: 关键字冲突
public abstract void teachersay(); // 抽象子类可以不重写抽象父类的的抽象方法
}
public class Test {
public static void main(String[] args) {
Student s = new Student(); // Person构造方法
s.say(); // i am student
s.work(); // person work
// Person p = new Person(); // 抽象类无法实例化, 因为抽象类中【可能】有抽象方法
}
}
由于抽象类无法被实例化,所以即使Teacher类继承了Person类,但是Teacher类依然无法实例化,所以抽象类父类如果没有非抽象子类继承的话是没有意义的。而同时,非抽象子类又必须实现抽象父类中的抽象方法。
所以abstract关键字的意义在于制订规范。只要父类制定好规范,子类就要按照这个标准来写。
典型的例子,就是集合体系。
public abstract class Collection implements Fetchable, Value, Filterable {.......}
接口
特点:类可以实现多个接口,接口可以继承多个接口。
接口中的所有方法都是抽象方法,所以接口不可实例化。接口中的成员变量都是静态常量。
实现接口的类必须重写抽象方法,或者该类本身是抽象类。
public interface Intf {
int num = 10; // 系统会默认加上关键字 public static final
void say(); // 系统会默认加上关键字 public abstract
static public final int num1 = 1; // public static final 三个关键字的顺序可以任意交换位置
final static public int num2 = 2;
}
public class Person implements Intf{
{
// num = 20; // 接口中的成员变量是常量,无法修改
}
@Override
public void say() {
System.out.println("i am person");
}
}
public class Test {
public static void main(String[] args) {
Intf p = new Person();
System.out.println(p.num); // 10 (接口中的变量是静态的)
p.say(); // i am person
}
}
之前谈到继承的时候,写过这样的话:
如果类之间可以多继承,那么在子类中就无法区分从多个父类中继承来的同名成员。
实现接口就不会存在这样的情况,因为接口中的方法全部都是抽象方法,而抽象方法是没有具体实现的,抽象方法最终都要子类去重写,所以实现的多个接口中出现同名方法并不碍事。
与此同时,接口中的成员变量都是静态常量,实现多个接口时可以用接口名直接调用,所以在成员变量上多实现也不冲突。
public interface Intf {
int num = 10;
void say();
}
public interface Intf2 {
int num = 20;
void say();
}
public class Person implements Intf, Intf2 {
@Override
public void say() {
System.out.print("implement Intf num is " + Intf.num);
System.out.println(", implement Intf2 num is " + Intf2.num);
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.say(); // implement Intf num is 10, implement Intf2 num is 20
}
}
接口和抽象类的区别:
- 抽象类:is的关系,体现继承体系中共性的东西,对内制定规范
- 接口:like的关系,体现继承体系的扩展功能, 对外提供规则
内部类
类也可以作为一个类的成员,这样的类称之为成员内部类,也叫内部类。
创建内部类对象有两种方法,第一种可以直接创建内部类对象,第二种可以在类的成员方法中创建内部类对象。
非静态内部类不可以有静态成员,因为随着类的字节码加载的时候,静态成员需要随之一起加载进来,但是此时内部类的字节码还没有加载。要做比喻的话,就好像人还没进屋子,心脏就进来了,这样是绝对不行的。
public class Person {
public Integer num = 10;
public class Inner {
// public static String name; // 非静态成员内部类中不可以有静态成员
public Integer num = 20;
public void print() {
Integer num = 30;
System.out.print("内部类方法num: " + num);
System.out.print(", 内部类成员num: " + this.num);
System.out.println(", 类成员num: " + Person.this.num);
}
}
public static class staticInner {
public static void print() { // 静态成员内部类才可以有静态成员
System.out.println("静态内部类的静态方法");
}
}
public void print() {
Inner in = new Inner(); // 通过成员方法创建内部类对象
in.print();
}
}
public class Test {
public static void main(String[] args) {
Person.Inner pi = new Person().new Inner(); // 创建非静态内部类对象
pi.print(); // 内部类方法num: 30, 内部类成员num: 20, 类成员num: 10
Person.staticInner psi= new Person.staticInner(); // 创建静态内部类对象
psi.print(); // 静态内部类的静态方法
}
}
局部内部类
存在于成员方法中的类,称之为局部内部类。
public class Person {
public void method() {
Integer age = 10; // jdk 1.8 之前,age需要定义为final
class Inner {
public void print() {
System.out.println("age: " + age);
}
}
Inner in = new Inner();
in.print();
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.method();
}
}
在jdk1.8版本之前,局部内部类想要访问局部变量,局部变量需要用关键字final修饰为常量。
在print()方法弹栈之后,method()方法随之弹栈,这时堆内存中的Inner对象没有引用指向它,但是垃圾回收机制并不是马上回收它,那么问题就出现了。
print()方法中使用了变量num,而num却已经随着mehod()方法的弹栈消失了。
所以虚拟机需要保证Inner对象在成为垃圾被回收之前还能访问到num,因此规定num需要成为常量,存放到方法区的常量池中,不会随着method方法的弹栈而消失。
匿名内部类
匿名内部类是局部内部类的一种。它和局部内部类的不同体现在:
- 匿名内部类没有类名
- 匿名内部类是抽象类或者接口的子类
使用匿名内部类一般只是使用其中一个方法,如果要使用类中的多个方法,反而没有使用局部内部类来的方便。、当然如果一定要使用匿名内部类,可以用父类引用指向子类对象。
public abstract class Teacher {
public abstract void study();
public abstract void teach();
}
public interface Student {
public abstract void study();
}
public class Person {
public void method1() {
new Student() { // 完整的样子: new [类名 extends] 抽象类/接口
@Override
public void study() {
System.out.println("student study");
}
}.study();
}
public void method2() {
Teacher t = new Teacher() { // 父类引用指向子类对象
@Override
public void study() {
System.out.print("teacher study");
}
@Override
public void teach() {
System.out.print(", teacher teach");
}
public void play() {
System.out.println(", teacher play");
}
};
t.study();
t.teach();
// t.play(); // 用父类引用指向匿名内部类无法调用匿名内部类的特有方法
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.method1(); // student study
p.method2(); // teacher study, teacher teach
}
}
在开发中,匿名内部类可以作为参数传递。下一章就会讲到集合,现在提前看这样一个例子:
public class Test {
public static void main(String[] args) {
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() { // 匿名内部类当做参数传递
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
}
}
public interface Comparator<T> {......}
这里创建TreeSet对象,给它传递一个实现Comparator接口的类,但是这个类创建出来只用一次,太浪费了。所以只创建一个匿名内部类作为参数传递过去。