面向对象二
2、三大特征
2.1、继承性
继承的好处:
- 减少了代码的冗余,提高了代码的复用性
- 便于功能的扩展
- 多态的前提
继承格式:class A extends B{}
- A继承B:A获取了B中声明的所有属性和方法
- B类中的private属性方法,子类任然获取到了,又因封装性影响,不能自接调用
- A类可以声明自己特有的属性方法:功能拓展
继承性规定
- 单继承:一个类只能有一个父类,但可多层继承
- 继承后:获取直接,间接父类中声明的属性和方法
- 没有显式声明父类,则继承于java.lang.Object 类
2.1.1、方法重写
重写:子类继承父类后,可以对父类中同名同参数的方法进行覆盖重写(方法名和形参列表需要一样)
应用:重写后子类对象调用同名同参方法,实际调用的是子类中的方法
重载和重写的区别
重写的规定
- 权限修饰符:子类重写的不小于父类的(不能降低可见性)
- 子类不能重写父类中声明为private的方法- 返回值类型:
- 父类 void 子类只能是 void
- 父类 A型 子类只能是 A类以及子类型
- 父类 基本类型 子类需要是相同的基本类型- 异常:子类 不大于 父类抛出的异常类型
- @Override:校验该方法是否是重写
- static:
- 子父类同名同参方法 为非static(考虑重写)
- 子父类同名同参方法 为static(不是重写)
- 静态方法不能被重写
2.1.2、子类实例化过程
从结果上:
- 子继承父,就获取了父类中声明的属性和方法
- 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。
从过程来看
- 创建子类对象,一定会直接或间接调用父类构造器,进而调用父类的父类构造器,直到调用了Object类中的空参构造为止。
- 正因为加载过所有的父类结构,所以才可以看到内存中父类中的结构,子类才可以考虑进行调用
明确:虽然创建子类对象时,调用了父类的构造器,但没有new所以只创建了一个子类对象
静态代码块 —> 构造代码块 —> 构造函数代码块
父类静态代码块—>子类静态代码块 ==>
父类构造代码块 —> 子类构造代码块 ==>
父类构造函数代码块 —> 子类构造函数代码块销毁顺序,是先销毁子类
2.2、多态性
多态是什么:
多态:一个事物的多种形态
父类引用子类对象(Person p = new Man)
多态的使用,虚拟方法调用
- 编译看左边,运行看右边
- 调用子类特有方法时编译不通过
- 调用子父类同名方法时,运行的是子类方法
- 调用父类特有方法时,子类继承的父类方法
- 只适用于方法,不适用于属性,属性编译和运行都看左边
为什么要有多态:
如果没有多态,会出现很多重载方法。
便于制定规范,各自不同实现由子类完成
调用子类特有方法(instanceof)
使用强制类型转换
强转时可能出现类型转换异常,使用instanceof检查
3、关键字
3.1、super
this:当前对象
super:父类的…
super 可以用来调用:属性、方法、构造器
- 子类方法/构造器:可以使用super.属性 或 super.方法 的方式,显式调用父类中声明的属性或方法。
- 子父类同名的属性,方法:使用this先找本类再找父类,使用super直接找父类
- super调用构造器
- super(形参列表),调用父类中声明的指定的构造器
- super需要出现在首行,和this只能二选一
- 在构造器中没有显示的调用,默认调用的是父类的空参构造
- 在类的多个构造器中至少有一个构造器调用了父类的构造器
4、常用类说明
String
String概述
String是一个final类,代表不可变的字符序列,是常量
String对象的字符内容是存储在一个字符数组value[]中的。
String实现了Serializable接口:表示字符串是支持序列化的。
String实现了Comparable接口:表示String可以比较大小
String虽然是引用类型,但也具备值传递的特性
字面量方式
(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
String s1 = "abc";//在常量池中产生一个字符数组,返回字符数据地址,在常量池中的位置
String s2 = "abc";//常量池中已经存在,返回存在的地址
System.out.println(s1 == s2); //true
String s2 = "bcd";
new String()
字符串拼接方式赋值对比
String s1 = "1abc";
String s2 = "ab";
String s3 = 1 + "ab" + "c";
String s4 = 1 + s2 + "c";
System.out.println(s1 == s3);//true
System.out.println(s1 == s4); //false
- JVM优化机制
String s3 = "ab" + "c";
会被处理成String s3 = "abc";
- 只要有一个变量参与结果就在堆中
- 可以调用
intern()
返回常量池中的值
常用方法
- length()
- charAt(int index)
- equals(Object obj)
- equalsIgnoreCase()
- substring()
- indexOf(String str)
- contains()
- replace()
- split()
- toLowerCase()
- toUpperCase()
- trim()
- toCharArray()
- getBytes()
- isEmpty()
- matches()
String转换
1、与基本数据类型、包装类之间的转换
String --> 基本数据类型、包装类:
调用包装类的静态方法:parseXxx(str)
基本数据类型、包装类 --> String:
调用String重载的valueOf(xxx)
使用字符串的 + 操作
2、与字符数组之间的转换
String --> char[]:
调用String的toCharArray()
char[] --> String:
调用String的构造器
3、与字节数组之间的转换
编码:String --> byte[]:调用String的getBytes()
解码:byte[] --> String:调用String的构造器
说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码。
StringBuffer & StringBuilder
String:不可变的字符序列;底层使用char[]存储
StringBuffer: 可变的字符序列;线程安全的,效率低;底层使用char[]存储
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储
StringBuffer内存解析
String str = new String();//char[] value = new char[0];
String str1 = new String("abc");//char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度是16的数组。
System.out.println(sb1.length());//
sb1.append('a');//value[0] = 'a';
sb1.append('b');//value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc");//char[] value = new char["abc".length() + 16];
//问题1. System.out.println(sb2.length());//3
//问题2. 扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。
默认情况下,扩容为原来容量的2倍 + 2,同时将原数组中的元素复制到新的数组中。
指导意义:开发中建议大家使用:StringBuffer(int capacity) 或 StringBuilder(int capacity)
常用方法
- append(xxx)
- delete(int start,int end)
- setCharAt(int n ,char ch) / replace(int start, int end, String str)
- charAt(int n )
- insert(int offset, xxx)
- length();
- reverse();
- for() + charAt() / toString()
StringBuffer作用
提高String的性能
Date
两个构造
Date()//创建一个对应当前时间的Date对象
Date(lone time)//指定毫秒值的时间
两个方法
toString()//转字符串
getTime() //获取毫秒值
SimpleDateFormat
Object
Object类是所有Java类的根父类
Object 类中方法
- clone()
- equals()
- finalize()垃圾回收之前调用
- getClass()
- hashCode()
- notify()
- toString()
- wait()
- notifyAll()
hashCode()
Object提供给我们了一个Native的方法“public native int hashCode();
Hash
Hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。关于散列值,有以下几个关键结论:
- 如果散列表中存在和散列原始输入K相等的记录,那么K必定在f(K)的存储位置上
- 不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞
- 如果两个Hash值不同(前提是同一Hash算法),那么这两个Hash值对应的原始输入必定不同
HashCode
1、HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的
2、如果两个对象equals相等,那么这两个对象的HashCode一定也相同
3、如果对象的equals方法被重写,那么对象的HashCode方法也尽量重写
4、如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置
HashCode 的作用
1、查找快捷:假设内存中有0-9的位置,现将 某字段 放入位置中,如不经过hash运算则任意存放,查找时需要遍历整个内存区域查找。当经过hash运算后存入的地址,在查找时可以直接到指定区域查找,从而提高查找效率。
2、产生hash碰撞后:ThreadLocal中的做法就是从算出来的位置向后查找第一个为空的位置,放置数据,HashMap的做法就是通过链式结构连起来
为什么重写Object的equals(Object obj)方法尽量要重写Object的hashCode()方法
public class HashCodeClass
{
private String str0;
private double dou0;
private int int0;
public boolean equals(Object obj)
{
if (obj instanceof HashCodeClass)
{
HashCodeClass hcc = (HashCodeClass)obj;
if (hcc.str0.equals(this.str0) &&
hcc.dou0 == this.dou0 &&
hcc.int0 == this.int0)
{
return true;
}
return false;
}
return false;
}
}
public class TestMain
{
public static void main(String[] args)
{
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
}
}
1901116749
1807500377
355165777
1414159026
1569228633
778966024
我们希望两个HashCodeClass类equals的前提是两个HashCodeClass的str0、dou0、int0分别相等。那么这个类不重写hashCode()方法是有问题的。
如果不重写,采用的是Object中的hashCode,根据对象地址值数学计算出得hashCode,每个对象是不同的,但对于equals相同的对象而言,也需要该对象的hashCode相同,这就是需要重写hashCode的原因
//Integer
public int hashCode() {
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
//String
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
//HashMap
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
clone()
https://www.cnblogs.com/fnlingnzb-learner/p/10649509.html
https://www.jianshu.com/p/0235759d8103
为什么要克隆
基本数据类型,字符串,包装类都有对于的副本机制
当试图保存对象的某时刻状态时,发现变量是浅拷贝。
class Student {
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = stu1;
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
}
}
如何实现克隆
一般步骤是(浅克隆):
-
被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)
-
覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)
下面对上面那个方法进行改造:
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
}
}
浅克隆和深克隆
浅克隆
深克隆
解决多层克隆问题
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。
public class Outer implements Serializable{
private static final long serialVersionUID = 369285298572941L; //最好是显式声明ID
public Inner inner;
//Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化]
public Outer myclone() {
Outer outer = null;
try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 将流序列化成对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
outer = (Outer) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return outer;
}
}
public class Inner implements Serializable{
private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
public String name = "";
public Inner(String name) {
this.name = name;
}
@Override
public String toString() {
return "Inner的name值为:" + name;
}
}
总结
实现对象克隆有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。
equals()的使用
== 和 equals
==:运算符
- 1. 可以使用在基本数据类型变量和引用数据类型变量中
- 2. 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型相同)
- 2. 如果比较的是引用数据类型变量:必将两个对象的地址值是否相同。equals():是方法,非运算符
- 只适用有引用数据类型
- Object 类中 equals()的定义:
public boolean equals(Object obj){ return (this == obj); }
- 通常情况下,我们自定义的类如果使用equals(),通常会重写父类的equals
toString()
object 中的toString:输出的是对象的地址值
需要输出指定格式内容,需要重写toString
Objects工具类 - 简化我们的操作
在JDK7版本的时候,Java引入了java.util.Objects工具类,用于封装一些平时使用频度很高或容易出错的操作,这些操作形成了Objects的各个方法,下来我们来看看这些方法。
1. Objects()构造方法
private Objects() {
throw new AssertionError("No java.util.Objects instances for you!");
}
这个方法没有特别的地方,但有两点需要特别注意:
- 该方法为私有方法
- 其实现为抛出一个异常(或错误)
其主要目的是为了避免对象的构造,即使通过反射机制也做不到!
2. equals()
有别于Object.equals()
,这个方法可以避免空指针异常。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
3. deepEquals()
Object.equals()
用于比较两个对象的引用是否相同,而deepEquals()
却扩展成了可以支持数组。
public static boolean deepEquals(Object a, Object b) {
if (a == b)
return true;
else if (a == null || b == null)
return false;
else
return Arrays.deepEquals0(a, b);
}
4. hashCode(Object o)
和Object.hashCode()
类似,只是在对象为null时返回的散列值为0而已。
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
5. hash()
生成对象的散列值,包括数组。
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
6. toString()
归根结底,其内部最终调用了对象的toString()
方法。只是额外多了空指针判断而已。
public static String toString(Object o) {
return String.valueOf(o);
}
public static String toString(Object o, String nullDefault) {
return (o != null) ? o.toString() : nullDefault;
}
7. compare()
用于比较两个对象。
public static <T> int compare(T a, T b, Comparator<? super T> c) {
return (a == b) ? 0 : c.compare(a, b);
}
8. requireNonNull()
在对象为空指针时,抛出特定message的空指针异常。
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null)
throw new NullPointerException(messageSupplier.get());
return obj;
}
9. isNull() 和 nonNull()
这两个方法用于判断对象为null和对象不为null。通常情况下,我们不会直接使用这两个方法,而是使用比较操作符==和!=。这两个方法主要用在jdk8开始支持的流计算里面。
public static boolean isNull(Object obj) {
return obj == null;
}
public static boolean nonNull(Object obj) {
return obj != null;
}
Comparable
是什么,为什么
Comparable 接口对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
实现Comparable的类必须实现compareTo(Object obj)方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
- 如果当前对象this大于形参对象obj,则返回正整数
- 如果当前对象this小于形参对象obj,则返回负整数
- 如果当前对象this等于形参对象obj,则返回零
实现Comparable 接口的对象列表(数组)可通过Collections.sort 或Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
如何实现
实现comparable接口
对于自定义类如果需要排序,让自定义类实现Comparable接口,重写compareTo()方法,在compareTo()方法中指定如何排序
Goods类
package com.zs.尹成.常用类.Object.comparable;
public class Goods implements Comparable{
private String name;
private double price;
public Goods(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
@Override
public int compareTo(Object o) {
if (o instanceof Goods){
Goods goods = (Goods) o;
//方式一:
if(this.price >goods.price){
return 1;
} else if (this.price < goods.price) {
return -1;
}else{
return 0;
}
}
throw new RuntimeException("传入的数据类型不一致");
}
}
测试类
public class ComparableTest {
@Test
public void test1(){
Goods[] arr = new Goods[4];
arr[0] = new Goods("z1",42);
arr[1] = new Goods("a1",22);
arr[2] = new Goods("z2",32);
arr[3] = new Goods("a2",2);
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
使用Comparator函数式接口,临时实现
@Test
public void test2(){
String[] arr = new String[]{"BB","AA","CC","EE","DD"};
Arrays.sort(arr, (o1, o2) -> -o1.compareTo(o2));
System.out.println(Arrays.toString(arr));
}
@Test
public void test3(){
Goods2[] arr = new Goods2[]{
new Goods2("z1",42),
new Goods2("a1",22),
new Goods2("z2",32),
new Goods2("a2",2)
};
Arrays.sort(arr, (o1, o2) -> {
if (o1.getPrice() > o2.getPrice()){
return -1;
}else if (o1.getPrice() < o2.getPrice()){
return 1;
}else{
return 0;
}
}
);
System.out.println(Arrays.toString(arr));
}
包装类
包装类实现软件的可拓展性
包装类:当作参数使用时,不会改变原来的数据(数据与地址是放在一起的)
同字符串做参数时也有相同的副本机制
Integer i1 = 10; //在高速缓冲区
Integer i3 = 10;
Integer i4 = 1000;
Integer i5 = 1000;//在堆上
Integer i2 = new Integer(10);//在堆上
System.out.println(i1 == i2);//false
System.out.println(i1 == i3);//true
System.out.println(i4 == i5);//false
//比较两个数是否相同
System.out.println(i4.intValue() == i5.intValue());
System.out.println(i4.equals(i5));
-
基本 --> 包装类
- 调用包装类构造器
Integer i = new Integer(1);
Integer i = new Integer("1");
Boolean b = new Boolean("true");
- 自动装箱
Integer i = 1;
- 调用包装类构造器
-
包装类 --> 基本
- 调用xxxValue()
int i = in1.intValue();
- 自动拆箱
int i = in1
- 调用xxxValue()
-
基本、包装 —> String
- 连接 +
String s = 1 + "";
- String的valueOf()
String s = String.valueOf(1);
String s = String.valueOf(new Bouble(12.4f));
- 连接 +
-
String —> 基本、包装
- 包装类的parseXxx()
int i = Integer.parseInt("1");
- 包装类的parseXxx()
面试题
//三元运算符,需要类型统一,会有自动类型提升
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1);//1.0
/**
包装类:将基本类型作为属性包装类的属性
如 private final int value;
Integer内部有一个内部类 IntegerCache
定义了一个缓存数组,Integer cache[],该数组中存储了高频对象-128~127
所以在范围内的值会在缓存中找,不在缓存中的则会创建新的对象
*/
public void method1() {
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
}