抽象类
class A { public A() { } public void show() { } }
一个普通类的基础上增加抽象方法,那么类便成了抽象类
abstract class A { public A() { } public void show() { } public abstract void s1(); public abstract void s2(); }
子类必须实现父类的所有抽象方法,不然也定义为抽象类
abstract class Achild extends A { @Override public void s1() { } }
class Achild extends A { @Override public void s1() { System.out.println("具体方法1"); } @Override public void s2() { System.out.println("具体方法2"); } public static void main(String[] args) { A a = new Achild(); a.s1(); a.s2(); } }
抽象类无法实例化,而必须通过子类来实例化
finalabstract class A {}抽象类有子类继承,因此无法使用final修饰
接口
interface Inter { }
接口中只能有全局变量和抽象方法,静态方法,默认方法
abstract class AbstractClass implements Inter{ public static final int x = 0;//接口中只能定义全局常量,并且需要初始化 //接口中的方法默认带有public public abstract void sum();//普通方法默认带有abstract public static void s(){ System.out.println("静态方法"); } public default void d() { System.out.println("默认方法"); } }
抽象类中可以不实现接口方法,但是子类需要全部实现上面方法
class Abstract extends AbstractClass{ @Override public void show() { System.out.println("我是子类方法show"); } @Override public void sum() { System.out.println("我是实现类方法sum"); } }
区别
不同点:
一个有构造方法,一个没有;
一个可以有普通方法和代码块,一个只能有抽象方法,静态方法和默认方法;
一个可以有常量和变量,一个只能有全局常量,并且需要赋值
类只能继承一个,但是接口能继承多个
相同点:
都不能被实例化,子类都需要实现或继承接口或者父类的所有抽象方法
重载与重写
public class overload { public void write() { System.out.println("我写了一本《山茶花》"); } public void write(String book) { System.out.println("我写了一本" + book); } } class override extends overload { @Override public void write() { System.out.println("我写了一本《山楂树》"); } @Override public void write(String book) { System.out.println("我写了一本《" + book + "》"); } public static void main(String[] args) { override child = new override(); child.write(); child.write("山楂树"); } }
重载发生在同一个类中,方法名相同、参数类型和个数、访问修饰符可以不同,调用的时候根据函数的参数来区别不同的函数
重写发生在子类中,方法名、参数列表、返回类型都相同,只是函数的实现体不一样。
被重写的方法不能比父类有更严格的权限,声明异常范围要小于父类方法
final和private修饰的方法不可重写
基本类型和引用类型
- 基本类型:就是值类型,即在变量所对应的内存区域存储的是值
- 引用类型:就是地址类型,在变量对应区域存储的是地址
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,成员变量存放在 Java 虚拟机的堆中,静态变量也存放在堆中,但属于类不属于对象。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中
- 默认值:成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
- 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址,所以要用equals()方法来比较
- Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能,范围内的对象重复使用会使用同一个缓存对象,超出对应范围才会去创建新的对象
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回True
orFalse,Float
,Double
不缓存Integer i1 = 127; Integer i2 = 127; Integer i3 = 128; Integer i4 = 128; System.out.println(i1 == i2);//true System.out.println(i3 == i4);//false
==和equals
==:对于基本类型,比较的是值是否相等;对于引用类型,比较的是地址的值是否相等
String str = new String("z"); String str2 = new String("z"); String str3 = "z"; String str4 = "z"; System.out.println(str3 == str4);//true 同一个常量池 System.out.println(str == str2);//flase System.out.println(str == str3);//flase
equals:比较的是对象是否相等,传入的是Object类所以不能比较基本类型
hashcode:比较对象的hash值是否相等
在比较两个对象是否相等时,一般先比较hashcode再比较equals,如果hashcode不相同那么对象必然不相同,如果相同就进一步调用equals()方法判断
String str = new String("z"); String str2 = new String("z"); String str3 = "z"; String str4 = "z"; System.out.println(str3.equals(str4));//true System.out.println(str.equals(str2));//true System.out.println(str.equals(str3));//true
public boolean equals(Object anObject) { if (this == anObject) { //比较地址是否相等 return true; }//判断是否是String类型,如果不是则false if (anObject instanceof String) { //转换为本地变量 String anotherString = (String)anObject; //计算字符长度,注意Java字符串末尾没有\0,因为对象都内置的有length方法,所以不需要计算长度。 int n = value.length; //如果长度不等,false if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) {//核心代码,逐个字符地比较 if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
HashMap
第一次调用 put 方法时创建一个长度是16 的数组
结合数组长度n采用位运算(n-1)&(h^(h>>>16))计算出向 Node 数组中存储数据的空间的索引值
如果计算出的索引空间没有数据,则直接将<key,value>存储到数组中
如果有数据,此时发生哈希碰撞,调用equals() 方法比较两个key是否相等
相等:将后添加的数据的 value 覆盖之前的 value
不相等:继续向下和其他的数据的 key 进行比较,如果都不相等则划出一个结点存储数据
如果结点长度即链表长度大于阈值 8 并且数组长度大于 64 则将链表变为红黑树
当超出阈值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的 2 倍,并将原有的数据复制过来。
size 表示 HashMap 中键值对的实时数量,注意这个不等于数组的长度。
threshold(临界值)= capacity(容量)* loadFactor(负载因子)。这个值是当前已占用数组长度的最大值。size 超过这个值就重新 resize(扩容),扩容后的 HashMap 容量是之前容量的两倍。默认的临界值是12 = 16 * 0.75
String
public final class String{ private final char value[]; }
final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象- String是不可变的字符串,因为String类内部的char字符串数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法,同时String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。- “+”和“+=”是专门为 String 类重载过的运算符,字符串对象通过“+”的字符串拼接方式,实际上是通过
StringBuilder
调用append()
方法实现的,拼接完成之后调用toString()
得到一个String
对象 。String str3 = new StringBuilder().append(str1).append(str2).toString();
- 赋值时不会重新赋值而是生成一个新的对象指向新的值,原对象被回收
- 在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个
StringBuilder
以复用,会导致创建过多的StringBuilder
对象,如果直接使用StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。String
中的equals
方法是被重写过的,比较的是 String 字符串的值是否相等。Object
的equals
方法是比较的对象的内存地址。
abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; }
StringBuilder
与StringBuffer
都继承自AbstractStringBuilder
类,在AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用final
和private
关键字修饰,并且AbstractStringBuilder
类还提供了很多修改字符串的方法比如append
方法。StringBuffer
对方法加了同步锁synchronized,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。- StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串,初始16个字符,扩容时2倍+2
深拷贝和浅拷贝
本质的区别是拷贝出来的对象的地址是否和原对象一样,也就是地址的复制还是值的复制的区别。
浅拷贝时,拷贝出来的新对象的地址和原对象是不一样的,但是新对象里面的可变元素的地址和原对象里的可变元素的地址是相同的
深拷贝的新对象的地址和里面的元素的地址跟原对象都不一样
反射
反射是通过获取类的class对象,然后动态的获取到这个类的内部结构,动态的去操作这个类的属性和方法
package streamof; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.SneakyThrows; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; @Data @NoArgsConstructor @AllArgsConstructor public class Person { private String name; private int age; private String code; private Person(String code) { this.code = code; } public void eat() { System.out.println("我是:小尘,我15岁"); } public void eat(String name, int age) { System.out.println("我是:" + name + ",我" + age + "岁"); } private void eat(String code) { System.out.println("我是星尘,我的编号是:" + code); } } class reflect { @SneakyThrows public static void main(String[] args) { Class clazz = Class.forName("streamof.Person"); //获取本类定义的成员变量,包括私有,但不包括继承的变量 Field[] fields = clazz.getDeclaredFields(); for (Field f : fields) { System.out.println(f.getName() + ":" + f.getType()); } //获取所有可见的方法,包括继承的方法 Method[] methods = clazz.getMethods(); for (Method method : methods) { Class<?>[] parameterTypes = method.getParameterTypes(); System.out.println(method.getName()+":"+Arrays.toString(parameterTypes)); } //获取所有可见的构造 Constructor<?>[] cs = clazz.getConstructors(); for(Constructor c : cs){ Class[] pt = c.getParameterTypes();//通过本轮遍历到的构造函数对象获取构造函数的参数类型 System.out.println(c.getName() + ":" + Arrays.toString(pt)); } //创建无参构造实例 Object o = clazz.newInstance(); System.out.println(o); //通过有参构造器创建实例 Object o1 = clazz.getConstructor(String.class, int.class, String.class).newInstance("小王", 12, "akd"); System.out.println(o1); //通过私有构造器创建实例 Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class); declaredConstructor.setAccessible(true); Object o2 = declaredConstructor.newInstance("a123"); System.out.println(o2); //调用无参数方法 Method eat = clazz.getMethod("eat"); eat.invoke(o1); //调用有参数方法 Method eat1 = clazz.getMethod("eat", String.class, int.class); eat1.invoke(o, "星尘", 15);//实例无论是o,o1,还是o2,都是无参数的o //调用私有方法,注意需要用Declareed Method eat2 = clazz.getDeclaredMethod("eat", String.class); eat2.setAccessible(true); eat2.invoke(o, "9999ff"); //动态修改对象的私有属性name Field name = clazz.getDeclaredField("name"); name.setAccessible(true); name.set(o2, "星尘"); System.out.println(o2); //动态获取对象的name System.out.println(name.get(o1)); } }
反射功能非常强大,并且非常有用,比如:
- 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
- 获取任意对象的属性,并且能改变对象的属性
- 调用任意对象的方法
- 判断任意一个对象所属的类
- 实例化任意一个类的对象
- 通过反射我们可以实现动态装配,降低代码的耦合度,动态代理等。
反射作用
- 运行时获取对象信息:通过反射,我们可以在运行时获取类的相关信息,比如类名、构造方法、成员变量、成员方法等,这对于某些框架、工具或者动态语言来说是非常有用的。
- 动态创建对象:通过反射,我们可以在运行时根据类名动态地创建对象。这使得我们可以更灵活地设计程序,比如根据配置文件中的类名动态创建对象。
- 动态调用方法:通过反射,我们可以在运行时动态调用对象的方法。这使得我们可以根据实际需要来动态选择需要调用的方法,而不是在编写代码时就静态固定下来了。
- 动态访问属性:通过反射,我们可以在运行时动态访问对象的属性。这使得我们可以更加灵活地设计程序,比如根据用户的输入来动态访问对象的属性。
线程
Thread类实现了Runnable接口并重写了接口的抽象方法run()
@FunctionalInterface public interface Runnable { public abstract void run(); }
class Thread implements Runnable { private Runnable target; public Thread(Runnable target) { this.target = target; } @Override public void run() { target.run(); } public void start() { start0(); } public void start0() { run(); } }
使得有参构造传入一个Runnable实例并执行该实例重写的run()方法
创建线程的三种方法
- 继承Thread类,重写run()方法,用start()调用
class Cat extends Thread { public void run() { int time = 0; while (true) { System.out.println("我是cat:" + Cat.currentThread()); try { // System.out.println(++time); Thread.sleep(1000); time++; } catch (InterruptedException e) { } if (time == 10) { break; } } } public static void main(String[] args) {//启动main线程 Cat cat = new Cat(); cat.start();//start新开了一个线程执行 cat.run();//普通的run方法代码,是main线程在执行 } }
- 实现Runnable接口并重写run()方法,实例化一个对象,new Thread(这个实例)并调用start()
class Dog implements Runnable { int count = 0; @Override public void run() { while (true) { System.out.println(++count); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { Dog dog = new Dog(); //dog.start()不能使用 Thread thread = new Thread(dog); thread.start(); } }
- 实现Callable接口,Callable接口与Runnable接口类似,但是有返回值
@FunctionalInterface public interface Callable<V> { V call() throws Exception; }
- 重写call()方法,实例化,用FutureTask包装然后调用start()方法
public class callable implements Callable<Integer> { @Override public Integer call() throws Exception { int i; for ( i = 0; i < 10; i++) { System.out.println(i); } return i; } public static void main(String[] args) throws ExecutionException, InterruptedException { callable c = new callable(); FutureTask<Integer> futureTask = new FutureTask<>(c); new Thread(futureTask,"有返回值的线程").start(); System.out.println(futureTask.get()); } }