Java笔记(一)


[TOC]

Java面向对象初级

一、方法的重载

  1. 注意事项和使用细节

    • 方法名必须相同;
    • 形参列表必须不同(形参类型、个数或顺序至少有一个不同,参数名无要求);
    • 返回类型无要求。
  2. 可变参数public int sum(int ... nums) 表示可接收多个参数。

    • 可变参数本质为数组;

    • 参数为0个或多个;

    • 实参可以为数组;

    • 可以与普通参数放在同一形参列表,但必须放在最后

    • 一个形参列表中只能有一个可变参数。

二、作用域

  1. 全局变量有默认值,可以不用赋值,局部变量无默认值,需要赋值。
  2. 属性可以与局部变重名,使用时遵循就近一致原则。

三、构造器

  1. 构造器是初始化对象,并非创建对象。

  2. 一个类可以定义多个不同的构造器,即构造器重载。

  3. 对象创建流程:

    • 对于以下代码:

      class Person{ //默认初始化
          int age = 90; //显式初始化
          String name;
          // 构造器
          Person(String n,int a){ //构造器初始化
          name = n;
          age = a;
          }
      }
      Preson p1 = new Person("小倩",20);
  • 加载Person类的信息(Person.class),只加载一次;

  • 在堆中分配空间(地址);

    • 完成对象初始化:

    • 默认初始化(age=0,name=null)

    • 显式初始化(age=90,name=null)

    • 构造器初始化(age=20,name=小倩)

  • 把对象在堆中的地址返回给p1(引用)

四、封装

封装的作用

  1. 提高代码的安全性;
  2. 提高代码的复用性;
  3. “高内聚”:封装细节,便于修改内部代码,提高可维护性;
  4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和写作。

封装主要代码

  1. get()set()函数,如下:
package demo;

import java.util.Scanner;

public class test1 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        person p = new person();
        p.setName("小明");
        p.setAge(600);
        System.out.println(p.getName());
        System.out.println(p.getAge());
    }
}
class person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        //数据校验
        if(name.length() < 2 || name.length() > 6)
        {
            System.out.println("名字长度不符合要求,默认张三!");
            this.name = "张三";
        }
        else {
            this.name = name;
        }
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        //数据校验
        if(age > 120)
        {
            System.out.println("年龄不符合要求,默认18!");
            this.age = 18;
        }
        else {
            this.age = age;
        }
    }
}    

封装与构造器

  1. 构造器可破解封装中的数据校验,所以可进行下列操作:
package demo;
   
   import java.util.Scanner;
   
   public class test1 {
   	public static void main(String[] args) {
   		Scanner scanner = new Scanner(System.in);
   		person p = new person();
   		p.setName("小明");
   		p.setAge(600);
   		System.out.println(p.getName());
   		System.out.println(p.getAge());
   		System.out.println("**********************");
   		person p1 = new person("小明",400);
   		System.out.println(p1.getName());
   		System.out.println(p1.getAge());
   	}
   }
   class person {
   	private String name;
   	private int age;
   
   	public person() {
   	}
   
   	public person(String name, int age) {
   		//破解了数据校验
   		this.name = name;
   		this.age = age;
   		//修复(构造器中调用set函数)
   		setName(name);
   		setAge(age);
   	}
   
   	public String getName() {
   		return name;
   	}
   	public void setName(String name) {
   		//数据校验
   		if(name.length() < 2 || name.length() > 6)
   		{
   			System.out.println("名字长度不符合要求,默认张三!");
   			this.name = "张三";
   		}
   		else {
   			this.name = name;
   		}
   	}
   	public int getAge() {
   		return age;
   	}
   	public void setAge(int age) {
   		//数据校验
   		if(age > 120)
   		{
   			System.out.println("年龄不符合要求,默认18!");
   			this.age = 18;
   		}
   		else {
   			this.age = age;
   		}
   	}
   }

五、继承

  1. 创建子类对象时,不论使用子类的那个构造器,默认情况下都会调用父类的无参构造器,若父类没有提供无参构造器,则必须在子类的构造器中用super去指定父类中的构造器(如super(String name,int age);)以完成父类的初始化,否则编译不通过。

  2. 若希望指定去调用父类的某个构造器时,需要显式的调用一下:super(参数列表)

  3. super()在使用时,必须放在构造器第一行(this()也是),故两方法不能共存于同一个构造器中。

  4. super()this()的区别:

    • super()访问父类中和子类重名的属性,若不重名,则和this()功能一致。

    • super()直接访问父类中的属性(方法),this()先在本类中找属性(方法),再去父类中找属性(方法)。

  5. 方法重载overload和方法重载override的比较:

    • 重载发生在子类,方法名一样,形参类别、个数或顺序至少有一个不同,返回类型和修饰符无要求;
    • 重写发生在父子类,方法名一样,形参类别、个数或顺序都相同,子类重写的方法的返回类型和父类返回类型一致或是其子类,子类的权限不能小于父类。

六、多态

方法的多态

  • 方法的重载和重写。

对象的多态

  • 一个对象的编译类型("="左边)和运行类型("="右边)可以不一致。

多态的向上转型

  • 本质:父类的引用指向子类的对象;

  • 语法:父类类型 引用名 = new 子类类型();

  • 特点:编译看左边,运行看右边;

  • 不能调用子类特有的成员。

Animal animal = new Cat(); //向上转型
Object obj = new Cat();
animal.eat(); //根据就近原则先去Animal中找相应的方法

多态的向下转型

  • 语法:子类类型 引用名 = (子类类型) 父类引用;

  • 只能强转父类的引用,不能强制父类的对象(对象创建后不再改变);

  • 当向下转型后,可以调用子类类型中的所有成员;

  • 要求父类的当前引用必须指向的是当前目标类型的对象。

Animal animal = new Cat(); //此时animal是指向Cat类型的对象
Cat cat = (Cat) animal;//编译类型和运行类型都是Cat
Dog dog = (Dog) animal;//编译通过,运行报错,违反要求:要求父类的当前引用必须指向的是当前目标类型的对象。

属性的“重写”

  1. 属性无“重写”之说,属性只看编译类型,即“=”左边。
A a = new B();
System.out.print(a.age); // 10(编译类型为A,找A的属性)
class A{
int age = 10;
}
class B extend A{
int age = 20;
}
  1. instenceof比较操作符,用于判断对象的运行类型是否为xx类型或xx类型的子类型。
B b = new B();//运行类型为B
System.out.print(b instenceof B); //true
System.out.print(b instenceof A); //true

A a = new B();//运行类型为B
System.out.print(a instenceof B); //true
System.out.print(a instenceof A); //true

Object obj = new Object();//运行类型为Object
System.out.print(obj instenceof A); //false
class A{}
class B extend A{}

动态绑定机制

  • 当调用对象方法时,该方法会与该对象的内存地址/运行类型绑定;

    (即若调用的方法子类没有而父类有,则先找到父类方法执行,但此时在父类方法中又需要执行子类和父类都有的方法时,需要看该对象的运行类型是哪个类,就执行那个类中的方法。重名的方法和对象的运行类型动态绑定)

  • 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。

多态数组

  • 调用共有方法
Person[] persons = new Person[3];//创建一个Person对象数组
persons[0] = new Person("Jack",20);
persons[1] = new Teacher("Jack",20);
persons[2] = new Student("Jack",20);
//循环遍历多态数组,调用say方法
for(int i = 0;i < persons.length;i++)
{
    //此时需要注意动态绑定机制,编译类型都为Person,运行类型根据实际情况看
    persons.say();
}
  • 调用特有方法(使用类型判断 + 向下转型)
Person[] persons = new Person[3];//创建一个Person对象数组
persons[0] = new Person("Jack",20);
persons[1] = new Teacher("Jack",20);
persons[2] = new Student("Jack",20);
//循环遍历多态数组,调用say方法
for(int i = 0;i < persons.length;i++)
{
    if(persons[i] instanceof Student){
    Student student = (Student)persons[i];//向下转型
    student.study();
    //((Student)persons[i]).study();
    }
}

多态参数

  • 方法定义的形参参数类型为父类类型,实参类型可以为子类类型]
Student student = new Syudent();
getName(student); //实参为student(子类)
void getName(Person p){}//形参为Person(父类)

七、Object类详解

equals方法

  1. ==equals的对比

    • ==既可以判断基本类型,又可以判断引用类型;
    • ==若判断基本类型(如比较数值大小时),判断的是值是否相等;
    • ==若判断引用类型(如比较两对象时),判断的是地址是否相等,即判断是否为同一个对象;
    • equals是Object类中的方法,只能判断引用类型;
    • equals默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等。
  2. 举例:Person类中equals方法的重写

    Person类中的equals重写,判断两对象地址或属性是否一样

package demo;
   
   public class test1 {
   	public static void main(String[] args) {
            Person p1 = new Person("a",10);
      		//Person p2 = p1;
            Person p2 = new Person("a",10);
            System.out.println(p1.equals(p2));
        }
   }
   class Person {
   	private String name;
   	private int age;
   
   	public Person(String name, int age) {
   		this.name = name;
   		this.age = age;
   	}
   
   	//Person类中的equals重写,判断两对象地址或属性是否一样
   	public boolean equals(Object obj)
   	{
   		if (this == obj)
   		{
   			return true;
   		}
   		if(obj instanceof Person)
   		{
   			Person p = (Person) obj;
   			return p.name.equals(this.name) && p.age == this.age;
   		}
   		return false;
   	}
}

hashCode

  1. hashCode的几点小结
    • 提高具有哈希结构的容器的效率;
    • 两个引用,如果指向的时同一个对象,则哈希值肯定是一样的,否则不一样;
    • 哈希值主要根据地址号来来的,不能完全将哈希值等价于地址;
    • hashCode也会重写。

toString方法

  1. 默认返回:全类名+@+哈希值的16进制,子类往往重写toString方法,用于返回对象的属性信息;
package demo;

public class test1 {
	public static void main(String[] args) {
		Person p1 = new Person("a",10);
		System.out.println(p1.toString()+" "+p1.hashCode());
		//输出:demo.Person@49e4cb85 1239731077
	}
}
class Person {
	private String name;
	private int age;

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
}
  1. 重写toString方法,打印对象或者拼接对象时,都会自动调用该对象的toString形式;
   package demo;
   
   public class test1 {
   	public static void main(String[] args) {
            Person p1 = new Person("a",10);
            System.out.println(p1.toString()+" "+p1.hashCode());
            //输出:Person{name='a', age=10} 1945604815
        }
   }
   class Person {
   	private String name;
   	private int age;
   
   	public Person(String name, int age) {
   		this.name = name;
   		this.age = age;
   	}
   	//重写toString方法,输出对象的属性
   
   	@Override
   	public String toString() { //重写后,一般输出对象的属性值,当然也可以自己定制
   		return "Person{" +
   				"name='" + name + '\'' +
   				", age=" + age +
   				'}';
   	}
}
  1. 当直接输出一个对象时,toString方法会被默认调用。(如:System.out.println(Person);就会默认调用Person.toString())
Person p1 = new Person();
System.out.println(p1);
//等价于System.out.println(p1.toString());

Java面向对象高级

一、 类变量和类方法

类变量(静态变量)

  1. 定义语法:访问修饰符 static 数据类型 变量名(推荐)或static 访问修饰符 数据类型 变量名

  2. 访问方法:类名.类变量名(推荐)或者对象名.类变量名

  3. 类变量是被类声明的所有对象共享的,实例对象是对象独有的;

  4. 类变量在类加载的时候生成,即对象未创建是也可使用;

  5. jdk8以前类变量存放在方法区,jdk8以后类变量放在里;

类方法(静态方法)

  1. 定义语法:访问修饰符 static 数据返回类型 方法名(){}(推荐)或static 访问修饰符 数据返回类型 方法名(){}

  2. 调用方法:类名.类方法名(推荐)或者对象名.类方法名

  3. 使用场景:将一些通用的方法设计成静态类,可以不创建对象就调用相关方法,如Math.sqrt()

  4. 类方法和普通方法都是随着类的加载而加载,将结构信息储存在方法区,但类方法中无this(),普通方法中隐含this()

  5. 类方法不允许使用和对象有关的关键字,如super()this(),普通方法可以;

  6. 静态方法只能访问静态成员,而普通成员方法,既可以访问普通变量(方法),也可以访问静态变量(方法)。

二、main方法语法

  1. 解释main方法的形式:public static void main(String[] args){}

    • main方法是虚拟机调用;
    • Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
    • Java虚拟机在执行main()方法时不必创建对象,所以方法必须是static
    • 该方法接收String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数,即java 执行的程序 参数1 参数2 参数3 ...

    有以下代码:

public class test1 {
	public static void main(String[] args) {
		for (int i = 0;i < args.length;i++)
		{
			System.out.println("第" + (i+1) + "个参数为:" + args[i]);
		}
	}
}

在命令行编译执行观察参数:

image-20220322115013733

三、代码块

  1. 代码块相当于另一种形式的构造器(对构造器的补充机制),可做初始化的操作;
  2. 代码块先于构造器加载;
  3. 静态代码块只在类加载时执行一次,普通代码块每创建一次对象就执行一次;
  4. 当只使用某个类的静态成员(未创建该类对象)时,该类的普通代码块不会被执行,但只要加载类时,静态代码块都会执行一次;
  5. 类什么时候加载:
    • 创建对象实例时(new)
    • 创建子类对象实例,父类会被加载,即先加载父类(及其代码块或构造器),再加载子类(及其代码块或构造器)
    • 使用类的静态成员时(静态属性、静态成员)
package demo;

public class test1 {
	public static void main(String[] args) {
		A a = new A(12);
		System.out.println("=======================");
		A b = new A("Hello");
	}
}
class A
{
	int a;
	String b;
	//静态代码块
	static
	{
		System.out.println("静态代码块只执行一次!");
	}
	//普通代码块
	{
		System.out.println("输出公共代码。");
		System.out.println("代码块无论放在哪个位置,都优先加载。");
		System.out.println("普通代码块每创建一次对象就执行一次!");
	};
	//构造器
	public A(int a) {
		this.a = a;
		System.out.println(a);
//		System.out.println("输出公共代码。");
	}

	public A(String b) {
		this.b = b;
		System.out.println(b);
//		System.out.println("输出公共代码。");
	}
}

/*
输出:
		静态代码块只执行一次!
		输出公共代码。
		代码块无论放在哪个位置,都优先加载。
		普通代码块每创建一次对象就执行一次!
		12
		=======================
		输出公共代码。
		代码块无论放在哪个位置,都优先加载。
		普通代码块每创建一次对象就执行一次!
		Hello
 */

四、类中调用的顺序(重点补充)

  1. 在继承的类中,构造器的前面其实隐含了super()和普通代码块,故调用顺序为父类中的代码块->父类中的构造器->子类中的代码块->子类中的构造器,示例如下:
package demo;

public class test1 {
	public static void main(String[] args) {
		B b = new B();
	}
}

class A //父类Object
{
	//super()
	//普通代码块
	{
		System.out.println("A的代码块被调用。");
	}
	//构造器
	public A()
	{
		System.out.println("A的构造器被调用。");
	}
}
class B extends A
{
	//super() 即父类A
	//普通代码块
	{
		System.out.println("B的普通代码块被调用。");
	}
	//构造器
	public B()
	{
		System.out.println("B的构造器被调用。");
	}
}

/*
输出:
	A的代码块被调用。
	A的构造器被调用。
	B的普通代码块被调用。
	B的构造器被调用。
 */
  1. 创建一个对象时,在一个类中调用的顺序是:

    • 调用静态代码块和静态属性初始化(两者优先级相同,若有多个,则按照定义顺序调用)
    • 普通代码块和普通属性的初始化(两者优先级相同,若有多个,则按照定义顺序调用)
    • 调用构造器方法
  2. 创建一个子类对象时(继承关系),调用顺序是:

    • 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    • 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    • 父类的普通代码块和普通属性(优先级一样,按定义顺序执行)
    • 父类的构造方法
    • 子类的普通代码块和普通属性(优先级一样,按定义顺序执行)
    • 子类的构造方法
  3. 补充:静态代码块只能调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员(包括静态成员)。

五、单例设计模式

设计模式:是在大量的实践中总结和理论化后优选的代码结构、编程风格以及解决问题的思考方式。


单例设计模式:采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式有两种方式:饿汉式和懒汉式(都要保证对象的唯一性)

饿汉式(线程安全)

  • 饿汉式在类加载时创建对象实例;
  • 饿汉式是不论是否要用某个对象,先将该对象创建好,故可能会出现多余对象,造成资源的浪费;
  • 饿汉式线程安全。

懒汉式(线程不安全)

  • 懒汉式在使用对象时才会创建对象实例;
  • 懒汉式是先定义唯一的对象,然后在调用对象时再创建对象,不会出现多余对象浪费资源的情况;
  • 懒汉式线程不安全。

演示代码SingleTon.java

package CodeBase.SingleTon;

//饿汉式
/*
步骤:
    1.将构造器私有化
    2.在类的内部直接创建对象(该对象是static)
    3.提供一个公共的static方法,返回创建好的对象
 */
class SingleTon_1
{
    String name;
    //先创建一个私有的静态对象
    private static SingleTon_1 singleTon_1 = new SingleTon_1("饿汉式");
    //构造器私有
    private SingleTon_1(String name) {
        this.name = name;
    }
    //公共静态方法返回创建好的对象
    public static SingleTon_1 getInstance()
    {
        return singleTon_1;
    }
}
//懒汉式
/*
步骤:
    1.构造器私有化
    2.先定义一个static对象,但不创建
    3.定义一个public的static方法,可以返回一个对象
 */
class SingleTon_2
{
    String name;
    //先定义一个对象
    private static SingleTon_2 singleTon_2;
    //构造器私有
    private SingleTon_2(String name){
        this.name = name;
    };
    //定义返回对象的方法
    public static SingleTon_2 getInstance()
    {
        if(singleTon_2 == null)
        {
            singleTon_2 = new SingleTon_2("懒汉式");
        }
        return singleTon_2;
    }
}
public class SingleTon
{
    public static void main(String[] args) {
        System.out.println("=============正常情况下============");
        SingleTon_Test singleTon_test = new SingleTon_Test();
        singleTon_test.singleTon1_test();
        System.out.println("==========================");
        singleTon_test.singleTon2_test();
    }
}
//测试对象是否位唯一
class SingleTon_Test {
    //测试饿汉式对象
    void singleTon1_test()
    {
        SingleTon_1 test_singleTon_1_1 = SingleTon_1.getInstance();
        SingleTon_1 test_singleTon_1_2 = SingleTon_1.getInstance();
        System.out.println(test_singleTon_1_1.name);
        System.out.println("饿汉式对象一地址:" + test_singleTon_1_1);
        System.out.println("饿汉式对象二地址:" + test_singleTon_1_2);
    }
    //测试懒汉式对象
    void singleTon2_test()
    {
        SingleTon_2 test_singleTon_2_1 = SingleTon_2.getInstance();
        SingleTon_2 test_singleTon_2_2 = SingleTon_2.getInstance();
        System.out.println(test_singleTon_2_1.name);
        System.out.println("懒汉式对象一地址:" + test_singleTon_2_1);
        System.out.println("懒汉式对象二地址:" + test_singleTon_2_2);
    }
}
/*
输出:
    饿汉式
    饿汉式对象一地址:CodeBase.SingleTon.SingleTon_1@22f71333
    饿汉式对象二地址:CodeBase.SingleTon.SingleTon_1@22f71333
    ==========================
    懒汉式
    懒汉式对象一地址:CodeBase.SingleTon.SingleTon_2@6aaa5eb0
    懒汉式对象二地址:CodeBase.SingleTon.SingleTon_2@6aaa5eb0
 */

###多线程情况下的饿汉式和懒汉式

未加线程锁演示代码Thread_SingleTon.java

package CodeBase.SingleTon;

public class Thread_SingleTon {
    public static void main(String[] args) {
        System.out.println("=============多线程情况下============");
        Thread_SingleTon_Test.Thread_Test_SingleTon_1();
        Thread_SingleTon_Test.Thread_Test_SingleTon_2();
    }
}

//多线程饿汉式
class Thread_SingleTon_1 implements Runnable
{
    @Override
    public void run() {
        //让线程停一会方便观察
        try {
            Thread.sleep(20);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 饿汉式:"+ SingleTon_1.getInstance());
    }
}
//多线程懒汉式
class Thread_SingleTon_2 implements Runnable
{
    @Override
    public void run() {
        try {
            Thread.sleep(20);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 懒汉式:" + SingleTon_2.getInstance());
    }
}

class Thread_SingleTon_Test
{
    //为饿汉式创建2个线程
    public static void Thread_Test_SingleTon_1()
    {
        Thread one = new Thread(new Thread_SingleTon_1());
        Thread two = new Thread(new Thread_SingleTon_1());
        //开启线程
        one.start();;
        two.start();
    }
    //为懒汉式创建2个线程
    public static void Thread_Test_SingleTon_2()
    {
        Thread one = new Thread(new Thread_SingleTon_2());
        Thread two = new Thread(new Thread_SingleTon_2());
        //开启线程
        one.start();;
        two.start();
    }
}

/*
输出(一种情况):饿汉式为同一个对象,懒汉式为不同的对象
    =============多线程情况下============
    Thread-1 饿汉式:CodeBase.SingleTon.SingleTon_1@3f58200d
    Thread-0 饿汉式:CodeBase.SingleTon.SingleTon_1@3f58200d
    Thread-2 懒汉式:CodeBase.SingleTon.SingleTon_2@59355dd6
    Thread-3 懒汉式:CodeBase.SingleTon.SingleTon_2@61034766
*/

多线程中懒汉式的改进

  • 为每个对象设置一个“互斥锁”,这表明,在每一个时刻只有一个线程持有该互斥锁,而其他线程若要获得该互斥锁,必须等到该线程(持有互斥锁的线程)将其释放。
  • 为了使用这个“互斥锁”,在JAVA语言中提供了synchronized关键字,这个关键字即可修饰函数,也可以修饰代码,实际上可以将其理解为就是一个锁,当一个线程执行该临界代码的时候,用synchronized给该线程先上锁,其它线程进不来,当线程代码执行完了的时候有释放该锁,只不过释放锁是隐式的不需要显示的指明,随代码的执行完毕,锁自动的被释放;
  • volatile关键字可以禁止指令重排。

演示代码:

package CodeBase.SingleTon;

public class Thread_SingleTon {
    public static void main(String[] args) {
        System.out.println("=============改进懒汉式==============");
        Thread_SingleTon_Test.Test_Fix_Thread_SingleTon_2();
    }
}
//多线程懒汉式改进
class Fix_SingleTon_2
{
    String name;
    //先定义一个对象
    private volatile static Fix_SingleTon_2 fix_singleTon_2; //第二层锁,volatile关键字禁止指令重排
    //构造器私有
    private Fix_SingleTon_2(String name){
        this.name = name;
    };
    //定义返回对象的方法
    public static Fix_SingleTon_2 getInstance()
    {
        if(fix_singleTon_2 == null)//第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        {
            synchronized (Fix_SingleTon_2.class)
            {
                //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
                //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
                if(fix_singleTon_2 == null)
                {
                    fix_singleTon_2 = new Fix_SingleTon_2("多线程懒汉式");
                }
            }
        }
        return fix_singleTon_2;
    }
}
//多线程改进懒汉式
class Fix_Thread_SingleTon_2 implements Runnable
{
    @Override
    public void run() {
        try {
            Thread.sleep(20);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 改进懒汉式:" + Fix_SingleTon_2.getInstance());
    }
}
class Thread_SingleTon_Test
{
    //为改进后的懒汉式创建2个线程
     public static void Test_Fix_Thread_SingleTon_2()
     {
         Thread one = new Thread(new Fix_Thread_SingleTon_2());
         Thread two = new Thread(new Fix_Thread_SingleTon_2());
         //开启线程
         one.start();;
         two.start();
     }
}
//此时对象唯一

六、final关键字

使用场景

  • 不希望类被继承时;
  • 不希望父类的某个方法被子类覆盖、重写时;
  • 不希望类中某个属性的值被修改时;
  • 不希望某个局部变量被修改时。

注意事项及细节讨论

  • final修饰的属性又叫常量,一般用XX_XX_XX命名;

  • final修饰的属性在定义是赋初值,并且不能再修改,赋值可以在下列位置:

    • 定义时:如public final double TAX_RATE=0.08
    • 在构造器中;
    • 在代码块中。
  • final修饰的属性时静态的,则初始化的位置只能在定义时和在静态代码块中,不能在构造器中赋值;

  • final类不能继承,但可以实例化对象;

  • 如类不是final类,但类里有final方法,则该方法不能被重写,但类可以被继承;

  • 一般来说,若一个类已经是final类,则不需要在此类中写final方法;

  • final不能修饰构造器;

  • final往往和static配合使用,效率更高,底层编译器做了优化:

    如下代码:

    • 直接访问static final定义的属性时,编译时就知道了a的值,所以直接访问不会初始化Final类,即static代码块不会被加载;
    • static final定义的属性需要某个方法来获取时,编译时无法知道b的值,只有运行时才能知道,所以在访问b的值时,需要先初始化类,即static代码块会被加载。
package demo;

public class test1 {
	public static void main(String[] args) {
		System.out.println("直接访问static final定义的属性:"+Final.a);
		System.out.println("=======================");
		System.out.println("访问static final定义的方法:"+Final.b);
	}
}

class Final //父类Object
{
	static final int a = 10;
	static final int b = b();

	static {
		System.out.println("Final类被加载。");
	}

	public static int b()
	{
		return 20;
	}
}

/*
输出:
	直接访问static final定义的属性:10
	=======================
	Final类被加载。
	访问static final定义的方法:20
 */
  • 包装类(Integer,Double,Float,Boolean)等都是final类,String也是final类。

七、抽象类和抽象方法

基本概述

  • 形式:abstract class A{}public abstract void B()
  • 当父类方法不确定时,考虑将该方法设计为抽象方法;
  • 所谓抽象方法就是没有实现的方法,即没有方法体;
  • 当一个类中存在抽象方法时,需要将该类声明为抽象方法;
  • 一般来说,抽象类会被继承,由子类实现其抽象方法。

使用细节

  • 抽象类不能被实例化;
  • 抽象类不一定包含抽象方法。包含抽象方法的类必须为抽象类;
  • abstract只能修饰方法和类;
  • 抽象类可以有任意成员,如非抽象方法、构造器、静态属性等;
  • 若一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为抽象类;
  • 抽象方法不能用private,final,static修饰,因为这些关键字都是和重写相违背的。

八、接口

基本介绍

  • 接口相当于提供了一个规范;

  • 接口就是给出一些没有实现的方法,封装到一起,到某个类要使用时,在实现这些方法。

interface 接口名{
//属性
//方法
}

class 类名 implements 接口{
//自己属性
//自己方法
//必须实现接口的抽象方法
}
  • 在jdk7.0以前,接口中所有方法都没有方法体,都是抽象方法;jdk8.0后,接口中可以有静态方法(static)和默认方法(default),即接口中可以有方法的具体实现。
interface A{
    //抽象方法可以省略abstract
    void a();
    //静态方法
    public static void b()
    {
        System.out.println("静态方法。");
    }
    //默认方法
    default static void c()
    {
        System.out.println("默认方法。");
    }
}

使用细节

  • 接口不能被实例化;
  • 接口中所有方法都是public,接口中抽象方法可以不用使用abstract
  • 一个普通类实现接口,必须将接口中所有方法实现;
  • 抽象类实现接口时,可以不用实现接口抽象方法;
  • 一个类可以同时实现多个接口,逗号间隔两个接口(class A implements IB,IC);
  • 接口中的属性只能是final的,而且是public final static修饰的,如int a = 1;,实际上是public final static int a = 1;即必须初始化;
  • 接口中的属性访问形式:接口名.属性(静态属性访问);
  • 一个接口不能继承其他的类,但能继承其他的接口:interface A extend B,C{}
  • 接口的修饰符只能是public和默认。

接口VS.继承

  • 继承相当于父子关系,子类生来就有(自动拥有)父类的一些属性和方法;而接口相当于师徒关系,某个类若想拥有某个接口的方法,则必须去学习(实现接口中方法)。
  • 如子类需要拓展功能,可以通过实现接口的方式,实现接口可以理解为对java单继承机制的一种补充。
  • 解决问题不同
    • 继承:主要提高代码的复用性和可拓展性;
    • 接口:设计、设计好各种规范(方法),让其他人去实现这些方法;
  • 接口比继承更加灵活
    • 继承满足is-a的关系,而接口只需要满足like-a的关系;
  • 接口在一定程度上实现代码解耦(即:接口规范性+动态绑定),低耦合,高内聚。

接口多态特性

  1. 多态参数
public class test1 {
   	public static void main(String[] args) {
   		//接口的多态体现
   		//接口类型的变量 if01 可以指向实现了接口的对象实例
   		IF if01 = new Car();
   		if01 = new Bar();
   		
   		//继承的多态体现
   		//父类类型的变量 a 可以指向继承A的子类的对象实例
   		A a = new B();
   		a = new C();
   	}
}
   
interface IF{}
class Car implements IF{}
class Bar implements IF{}

class A{}
class B extends A{}
class C extends A{}
  1. 多态数组
/*
给Usb数组中,存放 Phone 和 Camera 对象,Phone 类还有一个特有的方法 call(),
遍历 Usb 数组,如果是 Phone 对象,除了调用 Usb 接口定义的方法外,还要调用 Phone 特有的方法 call()。
 */
public class test1 {
	public static void main(String[] args) {
		//多态数组-> 接口类型数组
		Usb usb[] = new Usb[2];
		usb[0] = new Phone();
		usb[1] = new Camera();

		for (int i = 0;i < usb.length;i++)
		{
			usb[i].work(); //动态绑定
			//类型判断 (类型的向下转型)
			if (usb[i] instanceof Phone) //判断运行类型是否为 Phone
			{
				((Phone) usb[i]).call(); //向下转型
			}
		}
	}
}

interface Usb{
	void work();
}
class Phone implements Usb{
	public void call()
	{
		System.out.println("手机可以打电话。");
	}
	public void work()
	{
		System.out.println("手机工作中。");
	}
}
class Camera implements Usb{
	public void work()
	{
		System.out.println("相机工作中。");
	}
}

/*
输出:
	手机工作中。
	手机可以打电话。
	相机工作中。
 */

接口多态传递

//多态传递演示
public class test1 {
	public static void main(String[] args) {
		IG ig = new Teacher();
		/* 如果 IG 继承了 IH 接口,而 Teacher 实现了 IG 接口,
		   那么相当于 Teacher 实现了 IH 接口,这就是多态的传递
		 */
		IH ih = new Teacher();
	}
}

interface IH{
	void work();
}
interface IG extends IH {}
class Teacher implements IG{//接口可以继承
	//当接口IH中有抽象方法时,IG也继承了该方法,故在 Teacher 类中也需要实现方法
	public void work()
	{
		System.out.println("工作中。。。");
	}
}

九、内部类

类的五大成员:属性、方法、构造器、代码块、内部类。

内部类最大特点就是可以访问私有属性。

内部类的分类

  1. 定义在外部类局部位置上(如方法内)

    • 局部内部类(有类名)
    • 匿名内部类(无类名)
  2. 定义在外部类的成员位置上

    • 成员内部类(无static修饰)
    • 静态内部类(有static修饰)

局部内部类

  1. 局部内部类定义在方法中代码块中,其本质仍是一个类;

  2. 可以直接访问外部类的所有成员,包括私有的;

  3. 不能添加访问修饰符,因为它的地位是一个局部变量,局部变量不能访问修饰符的,但可以使用final修饰,以保证其不被继承,因为局部变量也可以使用final;

  4. 作用域:仅在定义它的方法或代码块中;

  5. 局部内部类访问局部内部类成员:直接访问;

  6. 外部类(内部类之外的第一个类)的方法中访问局部内部类成员:创建对象,再访问;

  7. 外部其他类(外部类之外的类)不能访问局部内部类;

  8. 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.this.成员去访问,如下代码演示。

public class test1 { //外部其他类
	public static void main(String[] args) {
		Outer outer = new Outer();
		outer.m1();
		System.out.println("outer对象的hashcode:"+outer);
	}
}

class Outer //外部类
{
	private int n1 = 100;
	public void m1() //方法
	{
		final class Inner
		{
			private int n1 = 200;
			public void f1()
			{
				System.out.println("n1="+n1); //就近一致原则,访问局部内部类的 n1=200
				//此时 Outer.this 相当于一个对象(调用m1方法的对象,此时为outer)
				System.out.println("n1="+Outer.this.n1); //访问外部类的 n1=100
				System.out.println("Outer.this对象的hashcode:"+Outer.this);
			}
		}
		Inner inner = new Inner();
		inner.f1();
	}
}
/*
输出:
	n1=200
	n1=100
	Outer.this对象的hashcode:demo.Outer@7a5d012c
	outer对象的hashcode:demo.Outer@7a5d012c	
 */

匿名内部类

  1. 本质详解

    有两个需求:

    • 需求1:使用IA接口,实现接口内方法
      传统方法:单独用类实现接口方法,并创建对象,如tiger1
    • 需求2:Tiger类只使用一次,后面不再使用,单独定义一个类浪费资源
      解决方法:使用匿名内部类简化,如tiger2

    解决代码:

public class test1 {
	public static void main(String[] args) {
		Outer outer = new Outer();
		outer.method();
	}
}

class Outer //外部类
{
	private int n1 = 100;
	public void method() //方法
	{
		//需求1
		Tiger tiger1 = new Tiger();
		tiger1.cry();
		//需求2:使用匿名类
		IA tiger2 = new IA()
		{
			@Override
			public void cry() {
				System.out.println("(匿名类)老虎叫。。。");
			}
		};
//		IA tiger2 = () -> System.out.println("(匿名类)老虎叫。。。");
		tiger2.cry();
	}
}
interface IA
{
	void cry();
}
//需求1
class Tiger implements IA
{
	@Override
	public void cry() {
		System.out.println("(实现接口)老虎叫。。。");
	}
}
/*
输出:
	(实现接口)老虎叫。。。
	(匿名类)老虎叫。。。
*/

分析:

  • tiger2的的编译类型是IA,运行内存就是匿名内部类;

  • 内部匿名类的底层:

    class Outer$1 implements IA //Outer$1是系统分配的名称
    {
        @Override
        public void cry() {
            System.out.println("(匿名类)老虎叫。。。");
        }
    }
    • Jdk底层在创建匿名内部类 Outer$1后,立即创建了Outer$1实例,并把地址返回给tiger2;

    • 匿名内部类只使用一次就不再使用(对象可以多次使用)。

  1. 匿名内部类的使用

    • 基于类的匿名内部类

      • father编译类型是Father,运行类型是Outer$1(匿名内部类);

      • 底层会创建匿名内部类:

      • 同时也直接返回了匿名内部类Outer$1的对象。

class Outer$1 extend Father
{
    @Override
    void speak() {
        System.out.println("匿名内部类重写 Father中speak方法");
    }
}

如下代码演示:

 public class test1 {
 	public static void main(String[] args) {
 		Outer outer = new Outer();
 		outer.method();
 	}
 }
 
 class Outer //外部类
 {
 	private int n1 = 100;
 	public void method() //方法
 	{
 		//基于类的匿名内部类
 		Father father = new Father("Jack"){ //匿名内部类也可使用构造器,Jack传递给构造器
 			@Override
 			void speak() {
 				System.out.println("匿名内部类重写 Father中speak方法");
 			}
 		};
 		System.out.println("father对象的运行类型:"+father.getClass());
 		father.speak();
 		//基于抽象类的匿名内部类
 		Animal animal = new Animal()
 		{
 			@Override
 			void eat() {
 				System.out.println("动物吃东西。");
 			}
 		};
 		animal.eat();
 	}
 }
 
 class Father
 {
 	String name;
 	public Father(String name) {
 		this.name = name;
 		System.out.println("接收到的名字:"+name);
 	}
 	void speak() {}
 }
 abstract class Animal
 {
 	abstract void eat();
 }
 /*
 输出:
 	接收到的名字:Jack
 	father对象的运行类型:class demo.Outer$1
 	匿名内部类重写 Father中speak方法
 	动物吃东西。
*/
  1. 使用细节

    • 调用可以动态绑定调用,也可以直接调用,内部类本身就会返回对象;

    • 可以访问外部类的所有成员,包括私有成员;

      • 匿名内部类访问外部类成员:直接访问;

      • 外部其他类(外部类之外的类)不能访问局部内部类;

      • 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.this.成员去访问。

//动态绑定调用
Animal animal = new Animal()
{
    @Override
    void eat(String s) {
        System.out.println("动物吃"+s);
    }
};
animal.eat("草");
//直接调用
new Animal() //返回对象
{
    @Override
    void eat(String s) {
        System.out.println("动物吃"+s);
    }
}.eat("草");
  1. 应用场景

    • 当作实参直接传递,如下代码演示:

    代码演示:

/*
需求:实现一个多功能闹钟,可以提醒不同事件
解决方法:1. 接口实现:代码繁杂
        2. 内部匿名类当作实参 
 */

public class test1 {
	public static void main(String[] args) {
		Outer outer = new Outer();
		outer.method();
	}
}

class Outer //外部类
{
	private int n1 = 100;
	public void method() //方法
	{
		CellPhone cellPhone = new CellPhone();
		//内部类直接当实参传递
		cellPhone.SmartClock(new Clock() {
			@Override
			public void ring() {
				System.out.println("起床了。");
			}
		});
		cellPhone.SmartClock(new Clock() {
			@Override
			public void ring() {
				System.out.println("上课了。");
			}
		});
	}
}

interface Clock
{
	void ring();
}
class CellPhone
{
	public void SmartClock(Clock clock)
	{
		clock.ring();//动态绑定
	}
}
/*
输出:
	起床了。
	上课了。
 */

成员内部类

  1. 定义在外部类的成员位置;

  2. 可以直接访问外部类的所有成员,包含私有的;

  3. 可以添加任意访问修饰符,因为它的地位就是类的一个成员;

  4. 作用域:和外部类的其他成员一样,作用域为整个外部类体;

  5. 成员内部类访问外部类:直接访问;

  6. 外部类访问内部类:创建对象再访问;

  7. 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.this.成员去访问;

  8. 外部其他类访问成员内部类:

/*
需求:外部其他类访问成员内部类成员
 */
public class test1 {
	public static void main(String[] args) {
		//方法1:相当于把new Inner()当作是outer的成员
		Outer outer = new Outer();
		Outer.Inner inner1 = outer.new Inner();
		inner1.say();
		//方法2:在外部类中,编写一个方法,可以返回Inner对象
		Outer.Inner inner2 = outer.getInner();
		inner2.say();
	}
}

class Outer //外部类
{
	private int n1 = 100;
	public class Inner {
		void say()
		{
			System.out.println("成员内部类输出。");
		}
	}
	//方法2中获取对象的方法
	public Inner getInner() //返回Inner类型的方法
	{
		return new Inner();
	}
}
/*
输出:
	成员内部类输出。
	成员内部类输出。
 */

静态内部类

  1. 定义在外部类的成员位置,并且有static修饰;

  2. 可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员;

  3. 可以添加任意访问修饰符,因为它的地位就是类的一个成员;

  4. 作用域:和外部类的其他成员一样,作用域为整个外部类体;

  5. 静态内部类访问外部类:直接访问所有静态成员;

  6. 外部类访问静态内部类:创建对象再访问;

  7. 若外部类和局部内部类的成员重名时,默认遵循就近一致原则,如果向访问外部类的成员,使用外部类.成员去访问;

  8. 外部其他类访问静态内部类:

/*
需求:外部其他类访问静态内部类成员
 */
public class test1 {
	public static void main(String[] args) {
		//方法1:静态成员可以直接通过 类名.成员 访问(满足访问权限时)
		Outer outer = new Outer();
		Outer.Inner inner1 = new Outer.Inner();
		inner1.say();
		//方法2:在外部类中,编写一个方法,可以返回Inner对象实例
		Outer.Inner inner2 = outer.getInner();
		inner2.say();
		//当getInner()为静态时,也可应直接用类名访问,如下
		//Outer.getInner().say();
	}
}

class Outer //外部类
{
	private int n1 = 100;
	static class Inner {
		void say()
		{
			System.out.println("静态内部类输出。");
		}
	}
	//方法2中获取对象的方法
	public static Inner getInner() //返回Inner类型的方法
	{
		return new Inner();
	}
}
/*
输出:
	静态内部类输出。
	静态内部类输出。
 */

文章作者: 一袖南烟顾
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 一袖南烟顾 !
评论
  目录