Java基础 自学讲义 2. OOP部分特性

嘤嘤嘤 终于学完OOP的一部分特性啦 整理一下目录吧~

目录:

一.对象与类
A.预定义类
B.Java类库中的LocalDate
C.用户自定义类

   1.构造器
   2.隐式参数和显式参数
   3.封装的优点
   4.基于类的访问权限
   5.私有方法
   6.final实例域
   7.静态变量和静态常量
   8.静态方法
   9.工厂方法
   10.main方法
   11.方法参数

D.对象构造

   1.重载
   2.默认域初始化
   3.无参数构造器
   4.显式域初始化
   5.参数命名
   6.用this调用另一个构造器
   7.初始化块
   8.finalize

E.包

   1.类的引入
   2.文档注释

二.继承
A.类、超类和子类

   1.子类定义
   2.覆盖方法(override)
   3.子类构造器
   4.多态和动态绑定
   5.组织继承
   6.强制类型转换
   7.抽象类
   8.受保护访问

B.Object类

   1.Object是所有类的超类
   2.equals方法
   3.hashcode方法
   4.toString方法

C.泛型数组列表
D.对象包装器与自动装箱(Wrapper and AutoWrapping)
E.变参方法
F.枚举类
G.反射

   1.Class类
   2.捕获异常
   3.利用反射分析类的能力
   4.在运行时使用反射分析对象
   5.利用反射编写泛型数组拷贝方法
   6.调用任意方法
   7.继承的设计技巧

一.对象与类

1.预定义类

在 Java 中, 任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。
比如在使用标准库中的Date类时,构造某个对象的新的实例的时候一定要使用构造器constructor去new一个类,直接定义的话相当于只是定义了一个指针;

Date birthday;
System.out.println(birthday);
//error: variable birthday might not have been initialized
//因为此时的birthday还没有初始化,还没有实例化,无法访问内容
//正确的做法:
Date deadline = new Date();
System.out.println(deadline.toString());
//也可以像下面这样把方法运用于新定义的类上
String s = new Date().toString();
System.out.println(s);

要注意,在使用Date类的时候,java会默认加载import java.sql.*;这个库,因为java.sql和java.util库中都有Date类,所以在调用的时候会提示 error: reference to Date is ambiguous ;

2.Java类库中的LocalDate

可以如下定义一个LocalDate的对象,不用使用构造器;

LocalDate Today = LocalDate.now();
// 这里没有用构造器去new一个新的LocalDate对象,这是另一种构造对象的方法
System.out.println(Today.toString());//2018-08-24
System.out.println(Today.getYear());//2018
System.out.println(Today.getMonthValue());//8
System.out.println(Today.getDayOfMonth());//24

----------

LocalDate Deadline = LocalDate.ofYearDay(894, 222);
System.out.println(Deadline.toString());
System.out.println(Deadline.plusDays(111));//调用plusDays()之后Deadline的值没有改变


之前使用的Date类里也有getYear() getMonth() getDay() 这些方法
但是Java不建议使用这些方法;比如getYear是获取的自1900年开始的年数,比如2018年的getYear()会返回118;
像plusDays这样,调用之后Deadline的值没有改变
只访问对象而不改变对象本身的方法称为访问器方法(Accessor Method);

最后是一个例子,做一个获取当前日期并打印当前月的日历的代码:

import java.time.*;

class test {
	public static void main(String[] args) throws Exception {
		System.out.println("Mon Tue Wed Thu Fri Sat Sun");
		LocalDate Today = LocalDate.now();
		int x=Today.getMonthValue(),y=Today.getDayOfMonth();
		Today = Today.minusDays(y);//这里的Today是当月第一天的前一天
		for(int i=1;i<= Today.getDayOfWeek().getValue();i++) System.out.print("    ");
		int k=1;
		while(Today.plusDays(k).getMonthValue() == x){
		//由于Today是当月第一天的前一天,所以当月的第一天就要直接+1
			System.out.printf("%3d ",k);
			if(Today.plusDays(k).getDayOfWeek().getValue()==7) System.out.println();
			k++;
		}
	}
}
/*结果:2018.8的日历
Mon Tue Wed Thu Fri Sat Sun
          1   2   3   4   5 
  6   7   8   9  10  11  12 
 13  14  15  16  17  18  19 
 20  21  22  23  24  25  26 
 27  28  29  30  31 
 */

感受是 在写Java程序的时候不要像以前一样想当然的以为某个值的类型就是int或者String之类的,但是Java没有类似Python一样可以查看数据的类型的函数Type,所以要尽量尝试一下,而且Java的编译器提示信息很丰富,可以很好的利用,熟悉一段时间之后应该会好一些;

3.用户自定义类

首先上一个自己写的People类

import java.time.*;

public class test {
	public static void main(String[] args) throws Exception {
		People John = new People("John", true, 1997, 1, 21);
	}
}

class People{
	public String name;
	private boolean isMan;
	private LocalDate Birthday;
	
	public People(String name, boolean isMan, int year, int month, int day){
		this.name = name;
		this.isMan = isMan;
		this.Birthday = LocalDate.of(year, month, day);
	}
	
	public String getName(){
		return this.name;
	}
	public boolean is(People others){
		return this.name.equals(others.name);
	}
	public boolean getGender(){
		return this.isMan;
	}
	public LocalDate getBirthday(){
		return this.Birthday;
	}
	public int ThisYearsAge(int some){
		int age = some - this.Birthday.getYear();
		if( age < 0 ) return -1;
		else return age;
	}
}

1.构造器

构造器写在类里面,大概长这样:

	public People(String name, boolean isMan, int year, int month, int day){
		this.name = name;
		this.isMan = isMan;
		this.Birthday = LocalDate.of(year, month, day);
	}

调用大概是这样:

People John = new People("John", true, 1997, 1, 21);

调用构造器的时候一定要new;
要注意构造函数一般是使用public修饰的, 因为在主函数中可能需要通过构造函数来create一个新对象;

2.隐式参数和显式参数

在自定义类内部访问私有属性的时候,可以使用this.name去访问,也可以把this去掉,但是就要注意,在局部不能出现和外部的实例域内同名的变量,否则会覆盖掉,所以最好还是写上this吧:D假装自己再写PY

3.封装的优点

不要编写返回引用可变对象的访问器方法,比如上面的getBirthday函数返回的是一个LocalDate对象,这个对象是不能被更改的,所以没有关系,但是如果返回的是Date类,就可以使用setTime来更改这个对象,从而修改这个对象的私有属性,导致在调用其他方法如果用到了这个私有属性,就会产生错误,所以如果是返回值是Date类的时候,我们尽量使用return this.Birthday.clone();

4.基于类的访问权限

现在看下面这个方法:

	public boolean is(People others){
		return this.name.equals(others.name);
	}

我们发现是可以访问others的私有属性name的,这是因为others也是People对象,所以在People类内是可以访问所有的People的私有属性的;

5.私有方法

我们认为公有数据是比较危险的,所以我们尽量把数据设置成私有属性;

6.final实例域

我们可以在field内声明变量的时候就将其定义为final类型的, 比如public final String name ;这说明在对象内部没有提供对name这个私有属性的修改器,即没有setName()方法,这样在这个变量被初始化之后就不能再修改了;

7.静态变量和静态常量

在定义属性的时候可以使用static修饰符来定义静态变量;(不太常用)
也可以使用static final来修饰定义静态常量;

private static int age = 0;//仅类内部可以访问
public static final double Pi = 3.14;//可以使用People.Pi来使用这个常量
8.静态方法

static的方法不能调用类内部的属性(即不能用this的属性),除了static的属性;

	public static double getPi(){
		return Pi;
	}

我们可以使用John.getPi()来获得Pi值,但是实际上getPi函数和John实例并没有关系,所以Java建议使用People.getPi()而不要用实例名,避免混淆;
通常在两种情况下使用静态方法:

1.不需要访问对象内部状态,所有的输入由显式参数提供;
2.只需要访问对象内部的static属性;

现在的static修饰符已经不是静态的意思了,可以将其理解为属于类且不属于类对象的变量和函数。

#####总结一下关于static修饰符的用法就是:
######当一个变量需要初始化加载时候,或者是经常被调用的时候可以加上static;
######用static修饰的方法可以直接被调用,比如Math.Pi;
######不用static修饰的需要先实例化一个对象后才可以被调用;
######static的方法只能调用static的变量,没有static的方法可以调用所有变量;

那么为何main函数也要使用static呢,比如我们现在写的People类的main,我们希望不用实例化People就可以直接调用People.main()  所以我们使用static;

9.工厂方法

工厂方法

10.main方法

在Java中,每个类都可以有一个main方法,比如People类也可以有一个main方法,main方法也是一个静态方法,这样做可以方便在大型项目中,对单个类进行调试,而不用编译运行整个项目代码;在运行整体项目代码的时候,独立的People类的main方法是不会调用的;

11.方法参数

Java中所有的方法参数都是按值传递的(call by value);所以下面这样的程序是不能成功的:

	public static void main(String[] args) throws Exception {
		People John = new People("John", true, 1997, 1, 21);
		People Amos = new People("Amos", true, 1983, 9, 13);
		swap(John,Amos);
		System.out.println(John.getName());
	}
	
	public static void swap(People u, People v){
		People x = u;
		u = v;
		v = u;
	}
//C也是传值调用,但是可以传指针的值,所以可以通过传指针来实现swap的功能;
//C++支持传引用调用,所以可以传要改变的值的引用来实现swap
//比如swap(People& u, People& v);

4.对象构造

1.重载

比如String的构造方法就有很多种,比如

String s = "CoreJava";//直接生成
StringBuilder a  = new StringBuilder("Core");//构造器StringBuilder
char[] x = {'J','a','v','a'};
String b = new String(x);//构造器String用数组生成

Java在构造对象的时候会自动解析参数匹配的构造方法,如果找不到,就会编译报错;
这个过程就是重载解析 Overloading Resolution

2.默认域初始化

如果构造器中没有把对象某些域赋予初值,Java会自动给这些域内的变量赋值;
int赋值0,boolean赋值false,其他对象都赋为null;

3.无参数构造器

一般的类都会提供一个无参数构造器
如果某个类没有自己写好构造器,Java会默认给提供一个无参数的构造器,按照上面的默认域初始化,但是如果自己写了构造器,则不能调用默认的无参数构造器,会报错;
如果希望对象能直接new People()来默认初始化可以这样写:

public People(){}
4.显式域初始化

初始化数据域也可以直接在声明中赋值,在定义的时候就初始化,比如String name = “”; 这在某些构建某些初始化相同的实例的时候比较有用,初始化的值也不一定是一个常量,可以是类里的某个方法;

5.参数命名

显式读入的参数通常的命名方法,建议不要使用单个字母比如String s,这样可读性比较差;
比如我们希望读入Name字符串的时候,我们可以使用String aName来做读入的参数名,也可以直接使用Name把实例域中的Name覆盖掉,在需要访问实例域中的Name时,使用this.Name;
也可以使用 _Name _mName _xName mName等特定字母+参数名的命名方法;

6.用this调用另一个构造器

在类的内部,当有多个构造器的时候,我们可以使用this方法,在一个构造器中调用另一个构造器,并且默认初始化一些值,比如:

	public People(String name, int age){
		this.name = name ;
		this.age = age;
	}
	
	public People(String name){
		this(name,12);
		this.isMan = true;
	}

//直接使用People John = new People("John");会把John的age属性初始化为12;

在执行完第一句this语句后,会继续执行后续的语句;

7.初始化块

在field后面之后可以用大括号写一个初始化块,会在对象构建,生命完域后,执行初始化块,比如:

class People{
	private String name ;
	private int age ;
	private boolean isMan;
	//下面是初始化块
	static
	{
		name = "";
		age = 0;
		isMan = true;
		System.out.println("Aluka ho ho ho ~");
	}

初始化块可以不止一个;
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块;使用static修饰符来标记初始化块即可;
补充一个生成随机数的tips

Random generator = new Random();
int x = generator.nextInt(10);//生成一个[0,10)范围内的随机整数
8.finalize

没太看懂,是个可能是个类似于C++里析构函数一样的垃圾回收作用的函数,但是因为Java里有自动的垃圾回收机制,所以这个函数好像是指不定啥时候运行…也没看到例子,以后再说吧

5.包

Java中的包是有层次嵌套的,一般用公司名的逆序来表示包名,比如baidu.com就会用com.baidu来表示百度的包;

1.类的引入

Java的编辑器会自动导入需要用到的包名,比如如果我使用LocalDate那么就会自动导入import java.time.*;当然也可以完整的写java.time.LocalDate,也可以导入的时候只导入import java.time.LocalDate;
还可以静态导入,以使用一些静态方法和静态域;
把类放入包中
类存储在文件系统的子目录中。 类的路径必须与包名匹配。
类文件也可以存储在JAR(Java 归档)文件中。在一个 JAR 文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省又可以改善性能。在程序中用到第三方 ( third-party ) 的库文件时,通常会给出一个或多个需要包含的 JAR 文件,JDK也提供了许多的JAR文件

2.文档注释

没看懂javadoc这个B东西咋搞, 以后再说吧;

文档注释

二.继承

1.类、超类和子类

前面我们写了一个人类,但是到这里我发现上面写的属性这里圆不过来了…所以我把人分成了富人和人…(嘤嘤嘤)
普通人手上只有现金cash,但是富人还有存款deposit,富人也是人,也有姓名年龄和现金,但是他比普通人多出了一份存款,而且他的getMoney方法也和普通人不一样,富人的钱等于cash+deposit,而普通人只有cash;
首先,Java里习惯把父类称作超类,继承的关键字是extends,写在子类的定义中,子类会继承所有父类的属性域和方法,但是不能直接访问父类的私有属性和方法(可以访问public的)在子类中称自己为this,称父类为super,子类要有自己的独有的属性,方法(包括构造方法);
下面先把整体例子给出来:

import java.util.*;

public class test2 {
	public static void main(String[] args) {
		People[] stuff = new People[3];
		RichPeople rich = new RichPeople("Recluse", 20, 1080, 3000);
		stuff[0] = rich;
		stuff[1] = new People("John",18,100);
		stuff[2] = new People("Amos",19,2000);
		rich.addDeposit(100);
		for(People u:stuff)
			System.out.println(u.getName()+" has "+u.getMoney()+" dollers~");
	}
}

class People{
	private String name = "";
	private int age = 0;
	private int cash = 0;
	
	public People(String name, int age, int cash){
		this.name = name;
		this.age = age;
		this.cash = cash;
	}
	
	public String getName(){
		return this.name;
	}
	public int getAge(){
		return this.age;
	}
	public int getMoney(){
		return this.cash;
	}
}

class RichPeople extends People{
	private int Deposit = 0;
	
	public RichPeople(String name, int age, int cash, int deposit){
		super(name,age,cash);
		this.Deposit = deposit;
	}
	public void addDeposit(int add){
		this.Deposit+=add;
	}
	public int getMoney(){
		int cash = super.getMoney();
		return cash+this.Deposit;
	}
}
1.子类定义

比如我想定义一个RichPeople作为People的子类,可以直接用extends来标识
class RichPeople extends People

2.覆盖方法(override)

直接在子类里写一个同名函数就可以了,但是因为在子类中不能直接访问父类的私有域,所以在获取cash值的时候不能直接this.cash,同时,因为正在覆盖写getMoney方法,所以也不能直接return this.getMoney()+this.Deposit所以我们需要用super来获取父类的方法,如下实现覆盖方法:

	public int getMoney(){
		int cash = super.getMoney();
		return cash+this.Deposit;
	}
3.子类构造器

要注意,在Java的继承中,子类是不继承父类的构造方法的
回忆一下,关键字 this 有两个用途:一是引用隐式参数,二是调用该类其他的构造器;
同样,super关键字也有两个用途: 一是调用超类的方法, 二是调用超类的构造器。
所以我们的子类构造器可以这样实现:

	public RichPeople(String name, int age, int cash, int deposit){
		super(name,age,cash);
		this.Deposit = deposit;
	}
4.多态和动态绑定
		People[] stuff = new People[3];
		RichPeople rich = new RichPeople("Recluse", 20, 1080, 3000);
		stuff[0] = rich;
		stuff[1] = new People("John",18,100);
		stuff[2] = new People("Amos",19,2000);
		rich.addDeposit(100);
		for(People u:stuff)
			System.out.println(u.getName()+" has "+u.getMoney()+" dollers~");

在遍历stuff数组的时候,可以用u既表示People类又表示RichPeople类,一个变量表示多种实际类型,这种现象就是多态(Polymorphism);
在调用方法的时候,Java会自动选择调用的方法,这是动态绑定(Dynamic Binding);

多态表明程序中出现超类对象的任何地方都可以 用子类对象置换。
Java中对象变量是多态的,不能将一个超类的引用赋给子类
在Java的多态中,上面的stuff声明的是People类型,但是在stuff[0]我将他指向了一个RichPeople对象,这样的子类引用指向父类对象的行为是可以的, 因为在不同的子类中重写了不同的父类方法,而每一个子类对象都一定也是一个父类对象;反过来父类引用指向子类对象的操作是不被允许的;
如果指向的类型不匹配会发生ArrayStoreException异常;

######总结一下,Java的多态就是指,声明变量时,可以声明为父类A,但是在构造对象,或者说实例化的时候,只能实例化为其任意子类B或者其本身对象A,如果声明为A的父类C则会产生ArrayStoreException异常;

关于动态绑定:
程序绑定是把方法和方法所在的类关联起来;
静态绑定有一些方法,是static, final, private的,或者是构造器,这两种情况是静态绑定的,因为可以确定这些方法所属的类一定是当前的类;
动态绑定有些方法,可能提供了多种不同的输入参数,所以在运行时需要根据具体对象的类型进行绑定,选择最合适的进行调用;

具体的Java方法调用过程和动态绑定过程如下:
Java方法调用过程和动态绑定过程

  附上一篇blog :    java的动态绑定与静态绑定(彻底理解多态与重载)

5.阻止继承

有的时候可能会希望某些类不能被其他子类继承,就可以把这个类标记为final类,这个类就不能再被继承了,比如String类就是final类,如果希望某些方法不能被重写,也可以使用final标记;
当整个类被标记为final之后,所有的方法都会自动变成final类,但是field域的内容不会变成final类;

6.强制类型转换

进行类型转换的唯一原因是: 在暂时忽视对象的实际类型之后, 使用对象的全部功能。
比如,我们可以定义一个父类的对象,然后引用某一个子类的对象,但是此时这个对象是不能直接访问子类的私有方法,如果想要访问子类的方法,需要用强制类型转换,如下:

//假若父类对象占1M的内存,因为子类对象有一些其他内容,所以多占用0.5M内存,子类对象一共占1.5M内存
People[] staff = new People[2];//这里我们定义了一个父类的对象,可以指向1M内存;
staff[0] = new RichPeople("John", 18, 100,1200);//开辟了1.5M内存,但是实际只指向了1M内存
staff[1] = new RichPeople("Tp",20,200,20000);
if(staff[0] instanceof RichPeople){//判断staff[0]是否是RichPeople的实例
	//先创建一个boss实例做暂存,把staff[0]做强制类型转换
	//因为boss的引用就是staff[0],所以对boss的操作就是对staff[0]的操作
	RichPeople boss = (RichPeople)staff[0];
	boss.addDeposit(10000);
	//也可以这样写:
	//((RichPeople)staff[0]).addDeposit(10000);
	//((RichPeople)staff[0])可以看做是创建了一个RichPeople类的匿名对象(等效于上面的boss)
	//这个匿名对象的引用和RichPeople类型的staff[0]是相同的	
}

######总结一下,强制类型转换可以理解为,强制类型转换可以把一个原本指向大内存的对象,改为指向某个小内存的对象,但是不能把一个原本为小内存的对象,转换成一个大内存对象,如果想这么做,需要用暂存对象或者匿名对象;

7.抽象类

可以使用abstract关键字来标记一个抽象类,比如:

abstract class Person{
	private boolean gender =true;
	public void getGender(){
		System.out.println(this.gender);
	}
}

一般用抽象类来标注一个比较通用的父类,一个抽象的类是可以被继承的,可以声明一个抽象类的对象,但不能引用一个抽象类的对象;
可以把一些方法标注为抽象方法,这就规定了所有由这个抽象方法继承出来的子类都必须要有这个抽象方法的重写(override),这是一种规定;
抽象类中不一定包含抽象方法,但是包含抽象方法的类一定要被声明为抽象类。抽象类本身不具备实际的功能,只能用于派生其子类。抽象类中可以包含构造方法, 但是构造方法不能被声明为抽象。
同时,使用了abstract修饰了类不能和final, private, static这些关键词共同使用来修饰一个类;

那么这样看来,可能会产生似乎抽象方法没啥用的错觉…因为即使在抽象类里写一个抽象方法,也可以在子类中写一个全新的方法来调用,其实不然,抽象方法至少有以下两个作用:
######1.当我声明了一个抽象类并且引用了一个其子类的对象,如果我希望调用这个对象的子类的某个方法,如果没有定义抽象方法,那么则无法直接使用这个方法,但是如果我在抽象类中定义了抽象方法,我就可以直接调用了;
######2.抽象类相当于是一种规范,它里面的抽象方法规定了所有的子类中都必须有这些方法, 所有的抽象方法都必须要被重写(override);

8.受保护访问

有的时候子类希望可以访问自父类继承的那些私有属性,但是因为private属性只能被自己的类访问,所以子类不能直接调用,可能要使用super,其实我们可以直接使用protected就可以在子类访问父类的属性了;
下面归纳一下 Java 用于控制可见性的 4 个访问修饰符:
1 ) 仅对本类可见 private
2 ) 对所有类可见 public
3 ) 对本包和所有子类可见 protected
4 ) 对本包可见—默认, 不需要修饰符

2.Object类

1.Object是所有类的超类

除了基本类型,所有的类都是从Object类继承过来的;
所有的数组,不论是基本类型的还是对象的,也都是从Object继承过来的对象;

2.equals方法

在很多类中,判断两个对象是否相等是一个很常用的方法,在java内置的要做判断相等操作有两种办法:
1.直接使用 = = == == 操作符, 这个操作符一般用于几种基本类型的判等操作,在用于对象之间的时候, A==B仅仅表示, A和B是否是相同的引用;
2.使用Objects.equals(Object A, Object B)来进行比较, 或者直接用A.equals(B)来进行比较,在超类Object中已经写好了equals的方法, 但是也是等效于 = = == == 的操作结果;
附上一张图来表示一下 = = == == 和equals之类的区别

还需要注意的一点是,Objects.equals是这样实现的:

public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

所以重写了子类的equals方法之后,调用Objects.equals(A, B)也会调用到子类的equals方法;

在这里区分一下Object和Object的区别: 附一篇blog

java.lang.Object
java中所有的 class 都继承自 Object


java.util.Objects
Objects 只是包含一些操作Object实例的工具方法,并且Objects 不能被实例化,它是final 的并且没有public的构造器。 只包含一些static 方法。

但是,实际上我们判断相等的条件往往不是严格到引用相等;

staff[1] = new RichPeople("Amos",20,100,10);
staff[2] = new RichPeople("Amos",20,100,10);
//此时的Objects.equals(staff[1], staff[2])其实是false的,因为这两者的引用并不相同
//但是其实这两个对象的所有基本信息都是相同的,往往我们希望在这时equals判定他们俩相同

我们可以通过对子类的equals方法的重写来实现这个判等的方法;
在编写equals的时候要遵循一下几个特性:
这里写图片描述

比较完美的equals的编写规范:
这里写图片描述

在这里附上一个我写的对RichPeople的判定:

public boolean equals(Object other){
		if(this == other) return true;
		//先判断引用是否相同
		if(other == null) return false;
		//判断other是否为null
		if(this.getClass() != other.getClass()) return false;
		//判断二者是否是同一个类
		RichPeople boss = (RichPeople)other;
		//判断域内的各个属性是否是相同的
		return this.getName().equals(boss.getName())
			&& this.getAge() == boss.getAge()
			&& this.getMoney() == boss.getMoney();
	}

其实不止是Objects, 数组也可以使用Arrays.equals()判断两个数组是否相等:
这里写图片描述

还要注意在子类中重写方法的时候, 显式的参数一定要和父类中的方法完全一样, 这样才能覆盖掉父类中的方法,否则就是完全写的一个新的方法;
Java建议在重写方法的时候在方法前面加上@Override标记, 这样如果这个新的方法不能完全覆盖掉父类的方法编译器就会报错;

3.hashcode方法

在比较的时候,可以用hashcode生成一个哈希值, 这样只要比较两个对象的哈希值, 基本上只要哈希值一样的两个对象就是相等的了;
Java中hashcode方法和equals方法是依赖的, 所以如果改变了equals方法就一定要改变hashcode方法;

比如在String中, hashcode方法就已经被重写过了, 是根据其内容生成的hashcode;
String的hashcode方法实现如下:

		String a = "aluka";
		System.out.println(a.hashCode());//92914816
		int hash=0;
		for(int i=0;i<a.length();i++) hash=31*hash+a.charAt(i);//hash值计算方法
		System.out.println(hash);//92914816

要注意, 在没有重写过hashcode函数的类中, 调用hashcode方法会自动使用Object.hashCode()方法,不是根据内容来生成的hash值了;
比如:

		StringBuilder a = new StringBuilder("Aluka");
		StringBuilder b = new StringBuilder("Aluka");
		System.out.println(a.hashCode());//607635164
		System.out.println(b.hashCode());//529116035

在自己写hashcode函数的时候,可以这样写:

	@Override public int hashCode(){
		return Objects.hash(this.getName(), this.getAge(), this.getMoney(), this.Deposit);
	}

附上一些看起来比较常用的关于hashcode的方法:
这里写图片描述

4.toString方法

在Object类中也自带有toString方法,;
默认的toString实现是:

getClass().getName() + '@' + Integer.toHexString(hashCode())

getClass()函数得到的是一个class java.lang.Class类型, 可以用getName()函数把它转换成一个String类型;
这个toHexString是一个转16进制的函数, 也有转二进制的 toBinaryString, 要转回十进制的话可以用Integer.parseInt(x.toString,10)
这个捞比toString看起来应该没啥用, 所以在自定义类里面一般都是自己写toString的,一般的写法是:

类名[属性1:...,属性2:...,属性3:...,...]

比如我的RichPeople的toString是这样写的:

	@Override
	public String toString(){
		return this.getClass().getName()+"[Name:"+this.getName()+",Age"+this.getAge()+",Money:"+this.getMoney()+"]";
	}
	//output: RichPeople[Name:Amos,Age20,Money:110]

3.泛型数组列表

Java里面的数组虽然可以用变量来指定大小,但是一旦指定了大小就不太方便改变数组的大小,为了解决这个问题就引入了ArrayList;
ArrayList中不能存基本数据类型, 语法大概是这样的:

		ArrayList<String> a = new ArrayList<>();
		a.add("Aluka");
		for(String i : a){
			System.out.println(i);
		}

可以用add方法来顺序增加元素,
可以用ensureCapacity来确定这个数组的大小(相当于执行了很多次的add操作,设定大小之后才能进行赋值之类的操作),
可以用size()来获取当前ArrayList的实际元素的数量,
用trimToSize()来锁定数组的大小(一旦锁定了,再改变就需要新建内存来存,反正比较耗内存也比较慢,要等确定不会再加入新的元素的时候再用,如果前面用ensureCapacity确定了数组的容量,可以用这个来削减数组列表的大小);
ArrayList有以下一些实用方法:
这里写图片描述
这里写图片描述

add方法虽然可以指定插入元素的index但是必须要保证ArrayList是连续的,否则会报错;
只能使用get方法来获得index位置的元素;
只能使用set方法来设置index位置的元素的值;(要注意不能设置超过size()范围的位置的值,否则要先ensureCapacity或者add增加容量)
remove方法除了可以接收一个index删除指定位置的元素之外,还可以接收一个Object对象, 会自动寻找并删除ArrayList中的某个特定元素;

某些遗留代码中可能存在有ArrayList没有指定类型参数的情况,这虽然是被编译器所允许的,但是因为数组列表中输入对象的不确定性,所以这样并不安全,即使在代码中使用强制类型转换也可能会出现warning,为了标注出这个情况,我们可以在进行强制类型转化的时候在代码前面加上@SuppressWamings("unchecked")来标注这个代码可能是有问题的;

4.对象包装器与自动装箱(Wrapper and AutoWrapping)

因为ArrayList中不能是基本数据类型, 但是如果想在其中放入基本数据类型的话, 就需要使用包装器把一些基本数据类型包装;
比如
基本数据类型包装
如果直接用a.add(3), 编译器会自动变为a.add(Integer.valueOf(3)); 这个过程就是自动装包(autowrapping);
下面是自动拆箱和自动装箱;

Integer a = 3;//自动调用Integer a = Integer.valueOf(3);装箱
a++;//自动拆箱进行自增操作后装箱

//自动进行的操作如下:
Integer a = 3;
int b = a.intValue();
b++;
a = Integer.valueOf(b);

正因为有自动拆箱和自动装箱的功能,所以这些包装过的类和基本数据类型用起来差不多,但是不同在于 = = == ==运算符是不一样的, 包装过后的类的 = = == ==在大多数时候仅用于判断二者是否引用相同,而非判断值;(int类型只能在-127到127范围内实现值的判断)
如果要判断值还是应该使用equals方法;
这里写图片描述

5.变参方法

Java中可以有一些参数数量可变的方法,比如下面我写的这个方法实现了在一些double中寻找出最大值:

	public static double max(double... value){
		double maxx = Double.NEGATIVE_INFINITY;
		for(double i : value){
			maxx = i>maxx ? i : maxx;
		}
		return maxx;
	}

在写参数的时候,double… value实际上在方法内部获得了一个value[]的数组, 然后对这个数组进行操作就可以了;

如果某个方法编写的时候传入的是一个数组, 那么其实也可以用上面的可变参来代替这个数组

public static double max(double value[])

比如上面的value数组就可以用double... value来代替做可变数量参数;

6.枚举类

因为在学C++的时候对enum就不太理解, 所以现在学java的枚举遇到了一些困惑,说一说我自己的理解吧:

首先,在java中,枚举类是一种特殊的类,他继承自父类java.lang.Enum, 所以在理解enum的时候大多数时候可以把它当做一个类来看待;
先放一个我自己写的weather类:

import java.util.*;

public class test3 {
	public static void main(String[] args){
		weather w = weather.valueOf(weather.class, "SUNNY");
		weather s = weather.WINDY;
		System.out.println(w.getCode());//1
		System.out.println(s.getCode());//2
		System.out.println(w.toString());//Sunny
		weather.Search();
		
	}
}

enum weather{
	SUNNY, WINDY(2), CLOUDY(3), RAINY(4);
	
	private weather(){}
	private weather(int code){
			this.code = code;
	}
	public int getCode(){
		return this.code;
	}
	public static void Search(){
		for(weather i : weather.values()){
			System.out.println(i);//自动调用i.toString()
		}
	}
	private int code = 1;
}

然后通过这个例子来熟悉一下枚举enum的基本结构:
1.首先,外围像定义类一样用关键词enum+enumName大括号搞起来;
2.在枚举定义的第一行,可以定义一些枚举的内容[下面有关于内容的理解], 定义的方式可以直接写下要定义的内容的名字, 这里可以调用这个枚举类的构造方法;
要注意的是,如果在枚举类中定义了构造方法, 那么在定义内容的时候就一定要使用构造方法, 否则会报错, 如果没有定义构造方法,那么就会默认使用默认的构造方法, 这一点就和其他类一样, 所以最好自己也手写一个默认构造方法;
[所谓内容] 就是这个枚举类型定义的变量, 值只能是这里定义的内容中的某一个, 否则会报错error: cannot find symbol;
3.在定义完内容之后,下面可以定义构造函数和一些常用的方法; 这里有一个奇怪的地方在于构造函数最好使用private修饰符, 因为希望枚举类只能在定义的时候确定下几种数据内容, 不希望后续添加内容, 否则枚举类就没有意义了… 而且实际上也并不能使用其他的修饰符比如public, 因为会报错:D哈哈哈, 如果省略了修饰符当然就会默认使用private修饰符啦~
4.一般把枚举类的field(属性)放在定义内容后面, 或者放在程序的最后都可以, 没有影响~而且一般枚举类的field都定义成private final 因为一般不希望改变枚举类的属性;
5.枚举类因为是继承自java.lang.Enum所以自然也继承到了一些常用的方法, 比如用weather.values()可以用于遍历枚举类的所有内容;
比如toString方法可以打印出当前内容的名字, 但是不可以用weather.toString因为这个方法不是static的;
比如valueof方法可以把一个字符串转换成某个枚举类的内容, 可以用weather w = weather.valueOf(weather.class, "SUNNY");这样来初始化一个枚举类的内容;
比如ordinal()方法可以得到某个内容在enum中声明的位置, 初始位置为0, weather.RAINY.ordinal()获取到的值就是3;
比如compareTo()方法可以获得两个枚举内容在声明中的位置的差别,比如

weather s = weather.SUNNY;
weather r = weather.RAINY;
System.out.println(s.ordinal());//0
System.out.println(r.ordinal());//3
System.out.println(s.compareTo(r));//-3  =0-3

最后注意一点,enum作为一个特殊的类, 写完后编译的时候会作为一个独立的类进行编译, 产生一个名为weather.class的文件;

7.反射

1.Class类

我们可以对任意的对象使用getClass()方法来获取这个对象的类名, 可以对class类使用getName()方法来获取String类型的类名;
在java的Class库中有一个forName方法, 可以用这个方法来把一个字符串转换为一个Class类的实例:

Random s = new Random();
Class<?> cl = s.getClass();//此处可以使用Class cl = s.getClass();但是更建议使用前者

再结合newInstance()方法, 可以创造一个和目标对象相同类的一个新的实例:

class test4 {
	public static void main(String[] args)throws Exception{
		Random s = new Random();
		Class<?> cl = s.getClass();
		Object a = Class.forName(cl.getName()).newInstance();
	}
}

之所以完整的贴上程序是因为, 如果直接运行那三行主要代码并不能成功运行, 需要在主函数中加上throws exception作异常捕获, 是因为使用了forName方法;

2.捕获异常

捕获异常的基本语法是:

		Random s = new Random();
		Class<?> cl = s.getClass();
//		try {
//			code
//		} catch (Exception e) {
//			
//		}
		try {
			Object a = Class.forName(cl.getName()).newInstance();
			System.out.println(a.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}

可以在Exception e的地方填写可能出现的异常, 比如这里可以写ClassNotFoundException
这个printStackTrace具有一些神奇的能力, 具体是什么能力我也不太明白, 反正效果大概是这样的:

java.lang.ClassNotFoundException: java.util.RandomW
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:291)
	at test4.main(Untitled.java:13)
3.利用反射分析类的能力

所谓反射, 就是比如现在程序中出现了一个对象(比如在不同代码模块交互的时候获取到的某些代码), 但是可能这个对象并不是我自己定义的, 所以我并不清楚其所有的细节, 我就需要对这个类进行一些测试, 得到关于这个类的一些信息, 然后根据这些信息去做一些事;

首先是关于Modifier关键词, 就是修饰符, 比如public private 这些;
可以使用getModifiers方法来获取某个类的修饰符:

test4 me = new test4();
String name = me.getClass().getName();
try {
	Class<?> cl = Class.forName(name);
	Class<?> supercl = cl.getSuperclass();
	System.out.println(Modifier.toString(supercl.getModifiers()));//public
	System.out.println(Modifier.toString(me.getClass().getModifiers()));//public
} catch (Exception e) {
	
}

可以使用getDeclaredConstructors方法来获取所有定义的构造器的方法, 返回的类型是Constructor类型的一个数组, 然后可以使用getParameterTypes()来获取构造器所需的所有元素, 返回的是一个Class[]数组;
当然还有getDeclaredMethods和getDeclaredFields这些方法, 用法基本都差不多,贴一个打印所给类所有方法的程序:

import java.util.*;
import java.lang.reflect.*;

public class test4 {
	public static void main(String[] args)throws Exception{
		Double me = new Double("12.21");
		String name = me.getClass().getName();
		try {
			Class<?> cl = Class.forName(name);
			Class<?> supercl = cl.getSuperclass();
			Method[] Methods = cl.getDeclaredMethods();
			for(Method i : Methods){
				System.out.println(i);
				System.out.print(Modifier.toString(i.getModifiers())+" ");
				Class<?> returntype = i.getReturnType();
				System.out.print(returntype.getName()+" ");
				System.out.print(i.getName()+"(");
				Class<?>[] parameters = i.getParameterTypes();
				for(int j=0;j<parameters.length;j++){
					 System.out.print(parameters[j].getName());
					if(j!=parameters.length-1) System.out.print(", ");
				}
				System.out.println(");");
			}
		} catch (Exception e) {
			
		}
	}
}

//结果如下:

//public boolean java.lang.Double.equals(java.lang.Object)
//public boolean equals(java.lang.Object);
//public static java.lang.String java.lang.Double.toString(double)
//.......中间太多了不贴了嘤嘤嘤.........
//public static boolean java.lang.Double.isFinite(double)
//public static boolean isFinite(double);
//public static double java.lang.Double.sum(double,double)
//public static double sum(double, double);
//public static double java.lang.Double.parseDouble(java.lang.String) throws java.lang.NumberFormatException
//public static double parseDouble(java.lang.String);

贴一些可能比较好用的反射的方法:
java.lang.Class
java.lang.reflect

4.在运行时使用反射分析对象
import java.util.*;
import java.lang.reflect.*;

public class test4 {
	public static void main(String[] args)throws Exception{
		Double me = Double.valueOf("12.21");
		String name = me.getClass().getName();
		try {
			Class<?> cl = Class.forName(name);
			Class<?> supercl = cl.getSuperclass();
			Field[] ff = cl.getDeclaredFields();
			for(Field i : ff) System.out.println(i);
			Field f = cl.getDeclaredField("value");
			f.setAccessible(true);
			System.out.println(f.get(me));//12.21
			f.set(me, 3.1415926535897932);
			System.out.println(me);//3.1415926535897932
		} catch (Exception e) {
			System.out.println(e);
		}
	}
}

上面是我自己写的一个用于测试反射的代码, 首先,Field[] ff = cl.getDeclaredFields();这个程序会输出Double类型的所有定义的field(这里如果使用cl.geyFields()的话, 程序只能获得所有的public的类):

public static final double java.lang.Double.POSITIVE_INFINITY
public static final double java.lang.Double.NEGATIVE_INFINITY
public static final double java.lang.Double.NaN
public static final double java.lang.Double.MAX_VALUE
public static final double java.lang.Double.MIN_NORMAL
public static final double java.lang.Double.MIN_VALUE
public static final int java.lang.Double.MAX_EXPONENT
public static final int java.lang.Double.MIN_EXPONENT
public static final int java.lang.Double.SIZE
public static final int java.lang.Double.BYTES
public static final java.lang.Class java.lang.Double.TYPE
private final double java.lang.Double.value
private static final long java.lang.Double.serialVersionUID

然后我希望能通过Field f = cl.getDeclaredField("value");来获得名为value的这个域, 但是因为private final double java.lang.Double.value这个域是一个私有属性, 所以在类的外部是没法直接访问的, 所以我开启了f.setAccessible(true);用于直接用get方法来访问Double类的私有属性, 甚至可以用set对这个属性进行设值, 同时Java会抛出一些警告:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by test4 (file:/Users/zhangpeiwen/Documents/CoreJava/CodeOfJava/) to field java.lang.Double.value
WARNING: Please consider reporting this to the maintainers of test4
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
5.利用反射编写泛型数组拷贝方法

现在希望写一个用反射机制来完成数组的拷贝的方法, 我们希望动态地创建数组:
代码如下:

	public static Object CopyOf(Object a, int newlength){
		Class<?> cl = a.getClass();
		Class<?> cll = cl.getComponentType();
		Object newArray = Array.newInstance(cll, newlength);
		System.arraycopy(a, 0, newArray, 0, Math.min(Array.getLength(a), newlength));
		return newArray;
	}

我们使用getComponentType()来获取数组的单个元素的类型, 返回值是一个Class类, 然后可以使用newInstance函数来创建一个新的数组;
在java.lang.reflect.Array包中有一些常用方法如下:

java.lang.reflect.Array

6.调用任意方法

现在希望能使用类似于函数指针的东西来调用任意的函数, 在java中函数指针也是一个类, 名为Method类;
先上一个测试代码:

import java.util.*;
import java.lang.reflect.*;
import java.text.*;

public class test4 {
	public static void main(String[] args)throws Exception{
		try {
			Method m1 = Math.class.getMethod("sqrt", double.class);
			Method m2 = test4.class.getMethod("square", double.class);
			DecimalFormat fm = new DecimalFormat("0.000");
			for(int i = 1; i<10; i++){
				System.out.println("sqrt("+fm.format(i)+ ") = "+ fm.format(m1.invoke(m1, i)));
				System.out.println("square("+fm.format(i)+ ") = "+ fm.format(m2.invoke(m2, i)));
			}
		} catch (Exception e) {
			
		}
	}
	
	public static double square(double x){return x*x;}
}

首先我们可以通过getMethod方法来获取某一个具体的Method, 需要提供方法名和返回值类型(这里可以提供的参数数量是可变的);
比如我使用了Method m2 = test4.class.getMethod("square", double.class);来获得我所写的test4类中的square方法, 然后就可以使用Method类的invoke方法来调用这个方法;
比如我使用m1.invoke(m1, i)来调用Method类的m1方法, 方法给入的参数有两个(Object Obj1, Object... Obj2), 这里的Obj1是指调用这个方法的对象, Obj2指的是m1方法的输入参数是Obj2, 这也就意味着即将使用Obj1.m1(Object... Obj2);这个方法;
如果希望调用的是一个static的方法, 那么可能不需要实例化, 也就不存在Obj1, 所以可以在Obj1参数的位置上传入一个null对象;
DecimalFormat方法是一个用于控制java中输出精度的方法, 所获得的返回值是一个String;

CoreJava提供了一个需要注意的点, invoke方法的返回值和传入的参数都是Object类型的对象, 这意味着必须要在使用invoke方法时注意使用类型转换,比如上面的代码中如果我希望使用double x = m1.invoke(m1, i);编译器会报错, 因为m1.invoke(m1, i)的返回值类型是Object类型的, 所以需要做强制类型转换:double x = (double)m1.invoke(m1, i);
但是,做强制类型转换也会有一个问题, 在于编译器可能将无法检查这些代码, 只有等到运行的时候才知道传入的对象能不能做强制类型转换, 这就比较捞, 所以java不建议我们老用这个骚操作,最好少用Method, 甚至少用反射.

7.继承的设计技巧

引用博客里看到的一段整理吧:
1.将公共操作和域放在超类
2.不要使用受保护的域【子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性;在Java程序设计语言中,在同一个包中的所有类可以访问protected域,而不管它是否为这个类的子类】
3.使用继承实现“is-a”关系
4.除非所有继承的方法都有意义,否则不要使用继承
5.在覆盖方法时,不要改变预期的行为
6.使用多态,而非类型信息【使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展】
7.不要过多的使用反射【反射机制对于编写系统程序来说极其实用,但是不适用于编写应用程序】

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值