123

当一个人追求生活的必需品时,他往往是勤奋和勇敢的
当一个人去追求生活的消费品时,他会懒惰而温和
当一个人去追求生活的奢侈品时,他会变得软弱,奢侈品越多,他越愿意用剩余的财富而不是以他的生命为代价来守卫其财富
接受苦难,亦是接受成材
守住苦难,亦是守住万里江山
Java:是你身心疲惫的避风港,灵魂寂寞的温柔乡
数据结构动态可视化实现网站:
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
什么是语法糖?增强for就是普通for循环的语法糖,看上去更简单,但底层本质一样,都是用Iterator迭代器来实现的。用Lambda表达式在写函数式接口时,Lambda表达式简洁的书写方式像是接口参数为匿名内部类时的语法糖,但底层实现又稍有不同,Lambda底层没有.class文件,因此执行起来会比匿名内部类更快一些。
黑盒测试:只关注输入和输出结果,不关注代码的具体窒息感步骤。白盒测试:根据输入数据一步步追踪代码的执行,考虑每行的逻辑是否合理,代码是否高效,输出是否达到期望等问题。白盒测试需要我们去写代码,而黑盒测试时不需要写代码的。Junit单元测试是白盒测试。
我们注意在intelij IDEA第一次进行@Test注解时首先要在漂红的代码左边的小灯泡里导入Junit4的包,然后我们进行测试,运行时只要控制台里是绿色就表明没有问题。注意@Before和@After两个注解的用法,不管程序是否出现异常,@Before注解下的语句都会最先执行,@After注解下的语句都会最后执行,这有点像try-catch语句中的finally语句,无论是否有异常,最终都会执行finally语句中的代码。
聊聊Java中的反射机制:反射是框架的设计灵魂,框架可看做半成品软件,比如我们开发安卓软件,那么Android Studio提供的安卓开发平台就给了我们一个安卓开发的框架。Java运行有三个阶段:源码阶段(Source),类对象阶段(Class),运行时阶段(Runtime),在第三个阶段时才在JVM上运行。Java在生成.class字节流文件后会将源码中类的构造方法,成员方法和成员变量加载为类对象,放在对应的Constructor,Method和Fields数组中。这一加载为对象并存入数组的过程就是反射机制。
思考一个问题,我们在Intelij IDEA开发环境下定义了一个字符串变量String str = “abc”; 当写到str.时会提示一堆相关的方法,这是怎么做到的呢?其实我们定义完字符串之后就有了相应的字节流.class文件,经过类加载器后类及其成员变量和成员方法会被封装送入相应数组中。那么我们得到类名时就能在数组中得到该类的所有成员变量和成员方法,得到对象时就能找到该对象的所有属性和方法。

谈谈反射中暴力反射的作用,我们知道,类中的私有成员变量在类外是不能直接访问的,C++中提供了友元来进行直接访问,而Java中没有友元,这时想要直接访问私有变量就要用到暴力反射,假如已经定义好了学生类,成员变量分数是私有变量:
Class studentClass = Student.Class();
Field scoreField = studentClass.getDeclaredField(“score”);
Student student1 = new Student(“黄浩”, 24, 120);
scoreField.setAccessible(true);
Object value = scoreField.get(student1);
System.out.println(value);
我们以前是有了学生,再用学生找分数,即student1.score,但现在不同了,是现有分数这个对象,再拿分数找学生并返回分数,即scoreField.get(student),这是反向的过程,因此被成为反射。
我们再看一看反射的具体案例,即反射实现了不改变该类的任何代码,可以创建任意类的对象,可以执行任意方法。我们先准备一个配置文件pro.properties,里面有:
className=
methodName=
接下来提前准备好了Student类,里面加入了public void eat() {sout(“吃”);}的方法,然后新建一个反射类Reflect,开始使用我们的反射机制来实现只改配置文件就达到更改类和方法的目的(像数据库连结池的操作)
Properties pro = new Properties();
ClassLoader classLoader = Reflect.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream(“pro.properties”);
pro.load(is);
String className = pro.getProperty(“className”);
String methodName = pro.getProperty(“methodName”);
Class aClass = Class.forName(className);
Object o = aClass.newInstance();
Method method = aClass.getMethod(methodName);
method.invoke(o);
接下来在配置文件中补全:
className = cn.itcast.day01.demo6.Student
methodName=eat
运行主函数即可。今后我们也应该注意到,只要配置文件中出现这种全类名的cn.itcast.day01.demo6.Student,就应当意识到是不是使用了反射。
聊聊注解:我们最常用的注解是@Override,重写,不必多说,其在编译阶段还有一个作用就是检测父类(或接口中)是否有该方法,若没有则直接报错。之前Junit测试时用过@Test注解,它是进行代码测试的标志,此外,还要记住@Deprecated,它表示在该注解下的方法已经过时,@SuppressWarnings,会压制该注解下所有方法的编译警告。
可进行中文编码的编码格式中,UTF-8中,一个汉字占3字节,GBK中一个字节占2字节。
聊一聊finally,记住核心一点是无论try{}中异常执行的成功与否,finally{}中的语句都会执行,特殊的一点是try{}中即使有return;也会执行finally,而有 return 时try方法体外的语句不再执行。只有当try{}中出现了死循环或有exit(1)等迫使操作系统强制性退出的语句时才会不执行finally,写在外面的语句和写在finally里面的语句到底有哪里不同呢?举个例子:
psvm{
int[] a = {1,2,3,4,5};
try {
int a[-1] = 1;
sout(111);
return;
} catch (ArrryIndexOutofBoundsException e) {
} finally {
sout(222);
}
sout(333);
}
因为a[-1]处直接跳去处理异常,所以最终结果是222和333
psvm{
int[] a = {1,2,3,4,5};
try {
int a[1] = 1;
sout(111);
return;
} catch (ArrryIndexOutofBoundsException e) {
} finally {
sout(222);
}
sout(333);
}
此时无异常,执行了return,结果是111和222
psvm{
int[] a = {1,2,3,4,5};
try {
int a[1] = 1;
sout(111);
} catch (ArrryIndexOutofBoundsException e) {
} finally {
sout(222);
}
sout(333);
}
想必已经猜到了,此时结果是111,222和333

聊聊Java多线程,假如我们有一个MyThread类:
public class MyThread extends Thread {
@Override
public void run () {
for (int i = 0; i < 10; i++) {
System.out.println(“son” + i);
}
}
}
在主函数里有:
public class Test {
public static void main () {
new MyThread().start();
for (int i = 0; i < 10; i++) {
System.out.println(“main” + i);
}
}
}
调用start()时是正确启用多线程的方式,JVM中把main压入方法栈后在执行的过程中遇到start()时会开辟新的栈空间,执行重写的run()方法,如果再调用一次new MyThread().start();就会再开辟一个新的栈空间,与之前的start和main并行执行。
而若把main中start改写为new MyThread().run(); 就不是启用多线程,只是普通的调用run()方法,此时run()被压入到与main一起的栈中,以单线程的方式依次执行run和main。

除了用Thread类来实现多线程外,还可以自己去写Runnable接口的实现类来实现,实现类定义一个对象,再将该对象作为参数传入到new Thread();中。具体形式为:
public class RunnableImpl implements Runnable {
@Override
public void run () {
for (int i = 0; i < 10; i++) {
System.out.println(“son” + i);
}
}
}
public class Test {
public void static void () {
RunnableImpl r = new RunnableImpl();
Thread t = new Thread®;
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(“main” + i);
}
}
}
这样比原来直接继承Thread类重写run方法启动线程的好处在于,我们可以在之后写不同的实现类,实现类中重写run写不同的任务,可以用同一个Thread对象来开启不同的线程任务,降低了程序的耦合性,即Thread t = new Thread(new RunnableImpl2); 如果是之前,那就要用不同的Thread 对象来实现不同的实现类,即MyThread t = new MyThread();
t.start(); MyThread2 t2 = new MyThread(); t2.start();
上述的Runnable在实际中其实有着更为简单的写法,我们知道当接口的实现类的方法只被调用一次的时候,我们常将其写为匿名内部类的形式,因此我们将Runnable的接口写成匿名内部类,这也是实际工作中的常用写法:
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(“son” + i);
}
}
};
Thread t = new Thread®;
t.start();
当然,熟悉了这种方式后还可以简化为更简单的究极写法:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(“son” + i);
}
}
}).start();
注意要把之前匿名内部类最后的分号给去掉。
眼熟么,在Android中我们调用子线程来更新UI的语法操作也是如此:
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getContext(), “加载失败”, Toast.LENGTH_SHORT).show();
}
});
public void run() {
Object obj = new Objext();
synchronized(obj) {
// 写需要互斥执行的代码
}
}
注意同步外的线程,未能获取锁对象时无法进入临界区,同步中的线程,直到线程执行完毕后才会释放锁对象。
线程池的引入:如果需要并发运行的线程很多,并且每个线程执行一个时间很短的任务就结束了,而线程的创建和销毁需要占用大量系统资源,频繁的创建和销毁线程就会导致系统的效率很低,我们试图去复用一个线程,Java线程池就应运而生
调用线程池中的线程方式有两种:
Thread t = list.remove(0);
Thread t = linked.removeFirst();
当使用完线程后,归还线程的方式也有两种:
list.add(t);
list.addLast(t);

File类的基本方法:
public String getAbsolutePath(); 返回此文件的绝对路径字符串
public String getPath();如果文件路径名是绝对的返回的就是绝对的,如果是相对的那么返回的也是相对路径
public String getName();返回最后一级的文件名(没有文件时返回最后一级的文件夹名)
public longlength();返回文件的长度(注意文件夹大小为零,只有最后一级有文件时大小才为文件具体所占字节数)
public boolean createNewFile();在构造方法路径下创建新文件
public boolean mkdir();在构造方法路径下创建新单级文件夹
public boolean mkdirs();在构造方法路径下创建多级文件夹
例如:
File f1 = new File(“D:\Program Files\1.txt”);
f1.createNewFile();
File f2 = new File(“D:\download\111\bbb\ccc”);
f2.mkdirs();
想遍历一个文件夹中的所有文件该怎么写?可以用dir.listFiles()的方法,
public static void getAllFiles(File dir) {
File[] f = dir.lisstFiles();
for (File file : f) {

System.out.println(file);

}
}
public staic void main (String[] args) {
File file = new File(“D:\download”);
getAllFiles(file);
}
其实仅用for循环只是遍历了一个文件夹中的文件夹和当前目录下的文件,像文件夹中还有文件夹,该文件夹里面还有文件这种深层次的文件并没有遍历到,那么想遍历到这种深层次的文件夹而且还不知道到底有多深的情况下,我们就可以用递归来完成。将上述方法改为:
public static void getAllFiles(File dir) {
File[] f = dir.listFiles();
for (File file : f) {
if (file.isDirectory()) {
getAllFiles(file);
} else {
System.out.println(file);
}
}
}
字节流用的是FileInputStream类,字符流用的是FileReader类,注意汉字编码中,UTF-8中一个汉字占3个字节,GBK中,一个汉字占2个字节,用字节流进行文件操作时,遇到汉字会编码为3个或2个字节,再转成char类型时会按照一个字节一个字节的转,这时就会出现乱码,因此建议在进行有汉字的文件的操作时最好使用字符流,即使用FileReader进行操作。字节流有缓冲写法如下(一个字节一个字节读入内存需要大量磁盘I/O操作,因此建立字节数组,成组的读入内存):
FileInputStream fis = new FileInputStream(“C:\user\hasee\photo\1.jpg”);
FileOutputStream fos = new FileOutputStream(“D:\download\2.jpg”);
byte[] bytes = new byte[1024];
int len = fis.read(bytes);
fos.write(byte, 0, len);
fos.close();
fis.close();
但文件中的内容一次大于1024字节时又会出现问题,所以用循环来读取:
FileInputStream fis = new FileInputStream(“C:\user\hasee\photo\1.jpg”);
FileOutputStream fos = new FileOutputStream(“D:\download\2.jpg”);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
fos.write(byte, 0, len);
}
fos.close();
fis.close();

不适用缓冲数组时写法为:
FileInputStream fis = new FileInputStream(“C:\user\hasee\photo\1.jpg”);
FileOutputStream fos = new FileOutputStream(“D:\download\2.jpg”);
int content = 0;
while ((len = fis.read()) != -1) {
fos.write((char) len);
}
还要注意FileWirter,即字符流写入类,在写入文件时需要先由JVM写入内存,由内存进行编码转化,即把字符流转换为字节流才能继续再写入文件中,因此会用到flush()刷新内存的方法进行这种编码的转化,如下:
FileWriter fw = new FileWriter(“D:\download\1.txt”);
fw.write(97);
fw.flush();
fw.close();
如何将类似于Map类的集合<key, valute>写道文件中去?除了可以建立Map<>对象,调用.toString()的方法外还可以使用Properties类,他原本是HashTable的子类,但HashTable因为单线程,效率低,早被HashMap替代,但Properties是唯一一个可以进行I/O操作的双列集合类,因此一直保留了下来,具体用法为:
Properties pro = new Properties();
pro.put(“迪丽热巴, “168”);
pro.put(“古力娜扎”, “170”);
pro.put(“马尔扎哈”, “175”);
FileWriter fw = new FileWriter(“D:\download\1.txt”);
pro.store(fw, “Person”);
fw.close();
当然,笨方法也有:
Map m = new HashMap<>();
m.put(“迪丽热巴”, “168”);
FileWriter fw = new FileWriter(“D:\download\1.txt”);
fw.write(m.toString());
fw.close();
之前讲到了手写细节数组来建立流的缓冲,其实Java中有专门为流进行缓冲的类,输出流缓冲类BufferedOutputStream和输入流缓冲类BufferedInputStream,其实这像是为战士穿上了一层铠甲,用法和之前手动建立的缓冲是相似的,同时注意要刷新,把数据刷到文件中:
FileOutputStream fos = new FileOutputStream(“D:\download\1.txt”);
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(“123”.getBytes());
bos.flush();
bos.close();
fos.close();

谈谈对 java 多态的理解?
多态是指父类的某个方法被子类重写时,可以产生自己的功能行为,同一个操作 作用于不同对象,可以有不同的解释,产生不同的执行结果。 多态的三个必要条件:
• 继承父类。
• 重写父类的方法。
• 父类的引用指向子类对象。
什么是多态
面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都 是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。 多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用) 实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所 引用对象的实际类型,根据其实际的类型调用其相应的方法。 多态的作用:消除类型之间的耦合关系。
现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发 生在不同的对象上会产生不同的结果。

1、 你所知道的设计模式有哪些?
Java 中一般认为有 23 种设计模式,总体来说设计模式分为三大类:创建型模式,结构型模式,行为型模式。其中创建型模式共五种,结构型模式共七种,行为型模式共十一种。

Q:讲讲指针和引用的区别?
A:指针占用内存空间,四个字节,引用不占用内存空间,占用的空间也是引用对象本身占用空间,引用可以说是外号和别名
指针可以是空指针,引用不可以,引用右边必须被赋予对象实体,否则会出现空指针异常。
指针的自加是对地址的操作,引用的自加是对引用对象的操作。

3.Java内存划分

内存泄漏:建立了资源就要及时释放(关闭),否则就会造成内存越用越小,即内存泄漏问题。比如读取文件时用的openFileStream流操作,打开数据库建立连接的DriverManager.getConnection()对象等,最后都要进行.close(); 当递归调用无正确的结束方式时,即无穷递归时也会出现内存泄漏问题,因为主函数中递归时每次递归调用都会在栈内存中开辟一块新空间,无穷递归时,栈内存肯定会被递归函数给爆掉从而造成了内存溢出。
内存泄漏与内存溢出的关系:每次使用流对象时不及时关流,导致一些对象白白占用空间而不被使用,相当于借钱不还,那么就会造成内存泄漏,长期内存泄漏堆积起来就会导致内存中一大块空间无缘无故不能使用,当我们再次申请比较大的一块空间时内存无法再满足我们,出现内存不够用的现象,此时就是内存溢出。
pc 寄存器在JVM中即程序计数器
方法区又称为元空间,里面存放的是.class文件,具体点,方法区中存放了常量,静态变量和类信息。(注意变量存在栈中,常量和静态变量这些不动的量是存放在方法区中)
栈中存放局部变量,存放上了处理机运行的方法,但宏观的看,栈实际是为线程划分了各自的空间,因此JVM中的栈也可以成为线程栈,栈中会为每个方法(包括主函数main)划分栈帧,每个栈帧中存放了局部变量表,操作数栈,动态链接和方法出口。局部变量表存放的是对象的引用,它指向了堆区域,即语句Person p = new Person();中p是引用,存的是地址,存在栈的局部变量表中,表中的地址指向new出的Person实体,即指向堆区域中划分出的一块空间。
方法区同栈一样,方法区中存放的常量和静态变量如果是对象(需要new)的话,那么也会存放地址来指向堆区域对应的空间。
JVM中处理.class文件时不少方法都是用C++来写的,说直白点,Java语言一定程度上要转化为C++后再执行
谈谈本地方法栈,运行线程new Thread().start();时,start()在底层会执行start0()的一种方法,此方法用的是native关键词修饰,这是一个本地方法,本质上它底层的一些方法是用C++写的,此方法需要的内存空间是怎么分配的呢?实际就是从本地方法栈中挖了一小块空间给该线程,这和线程栈开辟栈帧分配给上处理机运行的各方法类似,不过线程栈是Java实现的方法,而本地方法栈是由C++来实现内存空间的管理。
4.Java中的垃圾回收机制
垃圾回收算法,常用的是可达性分析算法,即根节点搜索算法。
一个对象经历过一次gc后,它的分代年龄就会加一,每次gc存活下来的对象都会被复制到下一块survivor中去,即存活的对象复制走,没能复制走的都是垃圾对象,一次性全部清空,效率非常高。当一个对象的分代年龄达到15时,如果还没被当成垃圾回收掉就直接挪到老年代中来。
毫无疑问,静态变量引用的对象最终都会变为老年代,像数据库连接池对应的Java的一些对象,像对象缓存池,包括Spring容器中的一些Bean,如Service Bean,最终都会挪到老年代。
当老年代都放满之后会触发full gc,会回收整个堆的内存空间。
Java调优的目的就是为了减少monitor gc减少full gc,总之gc越少越好。因为gc一次,处理机就会花很大的资源去处理垃圾,那么用户在网站上下订单这样的线程就会暂停,给用户的使用感受就是稍微卡顿了一下。gc结束后,线程又回复正常,用户又可以继续下单,这给用户的体验感受是不好的,gc多了会感觉网站很卡顿。每次gc时会调用STW(stop the world),正是它暂停了用户的线程。为什么gc过程中要调用STW去暂停用户线程呢?因为不暂停用户线程的话用户线程会继续执行,动作可能比gc还要快,这样之前不是垃圾的对象因为用户线程结束而释放内存资源的过程中对象引用的消失就会使得这些对象突然变成了垃圾,新的垃圾会导致gc想去再次采用可达性分析算法重新遍历所有对象来找到垃圾对象,这使得垃圾回收算法变得十分低效,于是索性gc时先暂停用户线程,先查找已有的垃圾对象并清除他们,虽然消磨了用户一点时间,但对于整体垃圾回收来说是高效的。
现有堆内存空间不变,对JVM调优,让其几乎不发生full gc的方法是存在的。可以在现有空间不变的情况下适当减少老年代的空间,增加年轻代的空间,这样存活周期短的对象会尽可能在年轻代空间中被回收掉,而不因为年轻代的存储空间限制瓶颈进入老年代最终导致老年代满,产生full gc。(简言之,让朝生夕死的对象尽量在年轻代被干掉,不要挪到老年代,挪到老年代积攒越多意味着以后越大几率调用full gc)

内存容量小的时候gc会比较块,因为遍历不算大的内存中的对象时比较快的,然而为了让总gc次数减少,往往需要大内存,一般来说新生代内存中monitor gc的算法要比老年代中full gc的算法块的多,但当伊甸园由较小的3G扩展到较大的30G时,monitor gc也不再快了,这时需要对其做一定优化。
JVM底层的垃圾回收实际是C++写的一堆代码(G1回收算法就是C++写的)

一线Java工程师需要做的

Java中的多态:形式为等号左边是父类引用,右边是new出的子类实体(“左父右子就是多态”)
调用成员方法和成员变量时的口诀:
成员方法:编译看左边,运行看右边。
成员变量:编译看左边,运行看左边。

类与类之间是单继承的,直接父类只有一个
类与接口之间是多实现的,一个类可以实现多个接口
接口可以汇集不同类间的一些共有的属性,去抽象出这种方法,然后定义在接口中,并在调用接口的实现类中进行具体的重写,即具体化抽象方法,虽然子类在继承父类后也可以重写父类方法,但接口的抽象方法中并没有方法体即大括号,这是更加凝练的形式的表达,也是接口和继承的一点区别所在。虽然父类中也可以写抽象方法,但抽象方法多用于接口中而非写在类当中。
父类 对象 = new 子类(); 属于向上转型,可以把子类当作父类看待。比如:
Animal object = new Cat(); 这就是创建了一只猫,并当作动物看待。
多态的目的在于右边的new出的东西可以随便换,而左边的父类引用不用改变,也就是任意一个这样的父类引用都能够调用父类的方法,而且相应的具有子类的一定标识。
左父右子是向上转型,向上转型一定是安全的,从小范围转向了大范围,类似于由int转向double,不会有精度的缺失。
向上转型也存在弊端,对象一旦向上转为父类,就无法调用子类原本特有内容,比如:
object.catchMouse(); 并不是所有的动物(动物是父类)都会抓老鼠,因此写法错误。这时如果仍想调用抓老鼠的方法,就要将对象向下转型为猫,即Cat object1 = (Cat) object; 这有点像基本数据类型的转换。
向下类型转换时一定要使用基本类型判断instanceof,否则会出现类转换异常ClassCastException,比如女朋友想要一只宠物,调用giveMeAPet()时传入参数即giveMeAPet(new Dog());此时在该函数方法体中就要判断这只宠物是什么if(animal instanceof Dog){}接下来在知道这宠物是狗之后才会有狗可以看家animal.watchHouse()和狗会吃肉animal.eatMeat()等狗的具体方法的使用
多态的本质就是使用时向上转型,形成抽象,由父类统一,方便使用者调用,底层再具体向下转型,回归对象本身,进行对象自身一些特有属性的调用。
多态的另一种使用情形时一个接口Collection,它有子接口List和Set。各自下面又有实现类ArrayList和HashSet。用多态写法可以创建对象Collection c1 = new ArrayList<>();也可以Collection c2 = new HashSet<>();。但想遍历c1时可以for循环,但c2集合是无序的,就不能用for,但他们都可以用Collection中的Iterator来进行集合遍历,这就很好了。知道循环次数时用for循环,不知道循环次数时用while循环,所以我们遍历上述集合c2 时可以:
Iterator iterator = c2.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
注意这里的迭代器的泛型Iterator要跟着要遍历的集合的类型走。注意增强for循环底层用的也是迭代器,不过只是形式上隐藏了迭代器的书写。
泛型通配符通常只在形参中出现,比如定义两个不同数据类型的ArrayList,此时想通过一个方法来打印两种数据。我们可以采用新建一个泛型类,然后在泛型类中写泛型方法,即:
public class Print {
public void method(E e) {
System.out.println(e);
}
}
也可以直接的用泛型通配符作形参去写方法,即:
public static void method(ArrayList<?> list) {
System.out.println(list);
}
Vector是ArrayList和LinkedList的前版本,Vector集合是所有单列集合的祖宗,从Java1.0版就有了(ArrayList和LinkedList从Java1.2版本才开始有),注意Vector是同步的,也就是单线程,单线程就一定会慢,这也是Vector被ArrayList和LinkedList替代的原因所在。
HashSet类底层是由HashMap实现的,它是无序的,因此不能随机访问,不能用普通的for循环来遍历,它是一个无序的,不重和的集合,因为底层是哈希结构,所以查询速度非常的快。JDK1.8版本前HashSet底层是由数组挂链表实现的,JDK1.8版本之后是由数组挂红黑树实现的(链表长度超过阈值8时,链表转化为红黑树)。HashMap的底层是由Map来实现的,不过Map是双列元素,HashMap只用了Map中的键列
注意Map是接口不是类,因此不能写成Map<String, Interger> map = new Map<>();我们应当用它的一些实现类来进行实例化,即Map<String, Integer> map = new HashMap<>(); Map中键是唯一的,如果有map.put(“a”, 123);map.put(“a”, 345);那么a最终对应的值是345,因为后者会把前者给覆盖了。
HashTable和HashMap的区别,HashTable不允许存空值空键(过时落后的表现),且HashTable是以哈希表为数据结构的集合的Java实现的早期版本(JDK1.0),HashTable和Vector在JDK1.2之后就被别的类所取代,ArrayList是取代了Vector而HashMap就是取代了HashTable(1.2以后),HashTable是线程同步的,即单线程(线程安全),这就意味着其执行很慢,因此之后都改用HashMap(多线程,线程不安全,但速度快,ArrayList也如此)。
但HashTable的子类Properties集合依然存活下来频繁使用,因为它是唯一一个和I/O流向结合的类。注意单列集合用add添加,双列集合用put添加
HashSet的底层实现原理:数组挂红黑树构成哈希表,如果有下面一段代码:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
HashSet h = new HashSet<>();
h.add(s1);
h.add(s2);
System.out.println(h);
其结果是[abc]。当h.add(s1);时,底层会先计算出s1的hashCode,也就是哈希值,为96354,查找哈希表中的已有数据的哈希值,发现没有重复的,就存入。当h.add(s2);时,底层同样操作后发现有96354,那么就再去调用equals()方法,即进行s2.equals(s1)的比较,发现两对象的值完全相同,那么此次就不存s2的值了,这样就实现了集合的不重复。
HashSet是彻底无序的,而LinkedHashSet是在HashSet的基础上加了一条维护序列的链表,因此用其存储的集合遍历后是有序的。
注意,抽象类一定不能是final类,因为抽象类中的抽象类型方法一定是要进行重写而final类则是不让类进行继承和重写,两者完全相反,水火不能相容。
关于静态,有一句话说得好,叫做静态与对象无关,确实,静态的调用是直接进行”类.方法()”的调用,用对象去点该静态方法反而会引起编译的错误。
对于成员变量的修饰符,其权限大小的对比是:
public > protected > (default) > private
其中平时省略不写权限时是归为default中,而非自动归为public 或private,C++中,不写是默认归为public。
当接口的实现类仅需要使用一次时可以不再单独写出该实现类,而在需要接口实现的地方写上匿名内部类。注意匿名内部类的写法,接口不是类,所以MyInterface myInterface = new myInterface();的写法是会编译报错的,那么我们已经了解匿名内部类的作用是省去单独写接口的实现类,匿名内部类的一般格式为:
MyInterface myInterface = new MyInterface(){
@Override
public void impl(){
// 重写接口中声明的抽象方法
}
};
注意,大括号是没有名字的类,而MyInterface是我们的接口而不是类,这个匿名内部类有名字,名字是myInterface,注意它不再是对象,而类的名字。也可以使用无类名的写法如:
new MyInterface(){
@Override
public void method(){
// 方法重写
}
}.method();
不过此种无类名的写法只适用于调用一次method()方法,如果method方法有两种,如method1()和method2(),那么就只能调用其中一种方法,即:
new MyInterface(){
@Override
public void method1(){
// 方法1重写
}

@Override
public void method2(){
// 方法2重写
}
}.method1();
method1().method2();的方法是行不通的,因为这是用方法一的返回值作为对象调用方法二,而方法一是没有返回值对象的。如果想两个方法都调用,就得定义类名后分开写,即:
MyInterface myInterface = new MyInterface(){
@Override
public void method1(){
// 方法1重写
}

@Override
public void method2(){
// 方法2重写
}
};
myInterface.method1(); // 类名调用方法
myInterface.method2(); // 类名调用方法
应注意区分的是,匿名内部类是省略了实现类,而匿名对象是省略了对象
我们来看一下:(Skill是接口,SkillImpl是接口实现类)
1.使用单独定义的实现类设置英雄技能:
hero.setSkill(new SkillImpl());
2.使用匿名内部类:
Skill skill = new Skill(){
@Override
public void use(){
System.out.println(“biubiubiu~”);
}
};
hero.setSkill(skill);
3.同时使用匿名内部类和匿名对象
hero.setSkill(new skill(){
@Override
public void use(){
System.out.println(“pia!pia!pia!”);
}
});
眼熟么,来看一看Android中的button触发事件的逻辑写法:
button.setOnClickListener(new View.OnClickListener{
@Override
public void onClick(View v){
Toast.makeText(getContext(), “您点击了按钮”, Toast.LENGTH_SHORT).show();
}
});
此处也是调用了点击聆听方法,方法中的参数正是匿名内部类和匿名对象的糅合。
直接打印对象的名字,就是调用对象的toString()方法,打出来的就是对象的地址值,如:
Person person = new Oerson();
System.out.println(person);
等价于
Person person = new Person();
String s = person.toString();
System.out.println(s);
判断一个类有没有重写toString()方法,直接打印这个类对应对象的名字即可,如果没有重写,打印出来的就是一串地址值,我们来重写上述Person类中的toString方法
@Override
public String toString(){
return “Person{name=” + name + “,” + “age=” + age + “}”;
}
浅谈B树和B+树的区别:
我们来看官方的一些回答:
B Tree:
叶结点具有相同的深度,叶结点的指针为空
所有索引元素不重复
结点中数据索引从左到右递增排列。
B+ Tree
非叶结点不存数据,只存索引,可以存放更多索引
叶结点包含所有索引字段
叶结点用指针相连,提高区间访问性能。
自己来解释如下:
如果是m阶的B和B+树,在树形上,B树的除根外的非叶结点的关键字个数为最小取到m/2上取整-1,最大取到m-1,B+树的树形则是两边比B树多1,即最小m/2上取整,最大m。在B树中,叶结点包含的关键字和其他结点(即非叶结点)所包含的关键字是互不重复的,而在B+树中叶结点就包含了非叶结点的全部关键字。此外,B树的叶结点通常为查找失败结点,实际中这些结点不存在,指向这些结点的指针为空。B+树中所有叶结点在同一层次上可以顺序由小到大排列,因此B+树除了可以像B树一样从根结点开始进行多路查找,也可以从叶结点的最小关键字开始进行顺序查找。B+树比B树更适用于数据库的查找操作。数据库查找性能取决于磁盘I/O次数,我们知道树的平均高度就是磁盘平均I/O次数,多路平衡查找树(B和B+树)相较于普通二叉搜索树的优势就在于多路平衡查找树比较矮胖,查找的次数就少,当然,也许会问,多路平衡查找树中一个结点中的关键字个数是很多个,而普通二叉搜索树一个结点中只有一个关键字。但要知道一个结点中的关键字查找实际上是内存或在Cache中进行的查找,内存读取的速度远快于磁盘读取的速度, 因此,树越矮胖越好,而B+树中非叶结点在实际中只存储对应子树的最大关键字和指针,也就是说B+树的非叶结点不像B树那样还去存储很多数据成员和每个数据成员的地址信息,也就是B+树的非叶结点只起索引作用,所有具体信息在叶结点便知分晓,因此B+树在非叶结点中存放的索引信息会更多,会比B树更矮胖,磁盘I/O次数更少,数据库性查找能更好。MySQL底层索引用的就是B+树。

注意到上图的B+树,空出的白块结点中存的实际是对应后面子块的地址空间。I/O操作其实也就是把子块加载到内存中来,这是最耗时的所在,而结点内部的折半查找非常快几乎不耗时,其用时连磁盘I/O的零头都不到。
谈谈数据库的索引:
索引是什么,好处:类似于为一本书创建目录,查书上的某个内容时不用再去一页页去翻,查找目录即可,极大的提高了查找效率。
索引的弊端:提高了查找速度,但降低了表更新的速度,因为更新表时不仅要维护表的数据信息,还要特意去维护索引相关的信息,建立索引也会占用磁盘空间,当数据量非常大,建立的索引又非常多时,索引文件就会随数据成员增加而急速膨胀,这就需要考虑如何建立最优索引。
索引的相关原理:前面说过,主流的RDBMS(数据库管理系统)都是把平衡树当做数据表默认的索引数据结构的,个别也有用哈希桶作为索引的数据结构的。建表时通常会指定好主键,一个没加主键的表,它的数据无序放置在磁盘上进行顺序排列,而加了主键后,表的存储结构就会由顺序结构转为树形结构(多路平衡树),整张表就变成了一张索引,也就是聚集索引,主键的作用就是把表的数据结构由顺序转换为树形结构。
索引和数据分离叫做非聚集,不分离叫做聚集。常用的数据库非聚集引擎有MyISAM。常用的数据库聚集索引有InnoDB。理论上查找时聚集引擎比非聚集引擎要快,因为聚集时查到叶结点找到索引时能立即将所在行的元素加载出来,因为数据同索引本就一起存在内存中,而非聚集在查找到叶结点时还要到另外一个文件(回表)中再做一次磁盘的I/O才能读取数据。
哈希结构也和B +树一样用于数据库底层索引引擎的算法,但一般不用哈希结构的原因是哈希不支持范围内查找,即我想在数据库中找年龄大于18的人是无法在哈希结构上去查找的。
谈谈平衡二叉树和红黑树的区别(为什么HashMap和HashSet用红黑树而不用平衡二叉树)?
平衡二叉树是严格平衡的,也就是每个结点的左右子树高度差不会大于1,这就使得它在相同结点数目的所有二叉树中是最“矮胖”的,树高最大限度的变低,也使得它的查找性能极优,平衡二叉树插入结点时维护树的平衡最多要两次旋转,和红黑树一样,但删除结点时维护平衡所需要的旋转次数会很多,为O(logn)量级,而红黑树最多只需3次旋转就能恢复平衡。但红黑树并非严格的二叉树,经常会出现一个结点的左右子树高度差为2的情况,所以查询效率上平衡二叉树高于红黑树(因为比红黑树矮胖),但插入和删除效率上红黑树更高。具体来说平衡二叉树每个结点都记录平衡因子,每次插入和删除都会先进行大量平衡度的计算再进行旋转,而红黑树结点中维护的是红黑颜色,插入和删除时仅是去换色和旋转,因此红黑树是性能和开销折中的一种选择。
要记住红黑树的一些特点:根结点总是黑色的,每个叶子结点总是黑色结点,不存在上下相连的红色结点,任一结点到叶结点的所有路径中经过的黑色结点个数都相等。
桶数组(哈希表)加链表好还是桶数组加红黑树好?
JDK8中HashMap在底层链表会在数据的结点数目大于8时自动转换为红黑树,就查找速度而言,链表是O(n),红黑树是O(logn),就添加方面而言,链表是O(1),红黑树是O(logn),可以说,结点少时使用链表方便且轻巧,开销小,而数据大起来时就要使用红黑树进行数据的操作。
注意在重写equals()等一些Object类中的方法的时候,Object类中的原equals()中传的是Object的对象,因此在自己定义的子类Person中重写equals()时要用到向下转型,比如:
public class Person{
private String name;
private int age;

public Person(String name, int age){
	this.name = name;
	this.age = age;

}

@Override
public Boolean equals(Object obj){
Person obj1 = (Person) obj;
return this.name.equals(obj1.name) && this.age == obj1.age;
}
}
格式化:某种类(比如Date类)的格式转向String类,
解析:String类转向该类(Date类)。
浅谈Java中的接口和继承:
Java中类的接口可以有多个,但类的继承只能有一个,即一个子类只能有一个父类,但一个子类可以有多个接口。C++中没有接口,但C++中一个子类可以继承多个父类。可以说,继承是类之间的关系,而接口是具体行为的实现。接口中的方法一般都是抽象方法且没有方法体,接口需要实现类进行实现,且实现类中要对接口中定义的抽象方法进行重写。虽然父类中也可以定义抽象类,也可以在子类中重写父类中的抽象方法。但接口较之于继承的好处在于接口所实现的功能上更单一,比如我想让看家的狗拥有猴子那样可以爬树的能力,我可以让狗去继承猴子类,但猴子类中也有倒挂在树上的特有方法,那么狗就不可避免的获取了倒挂在树上的方法,狗会倒挂在树上,这是伤风败俗的事,为了尽可能减小这种继承时带来的副作用,就要把猴子会爬树和会倒挂的两个方法写成两个接口,在狗类中只去实现爬树的方法就好了。
String和StringBuilder的区别
String定义出的时字符串常量,定义之初就已经在内存中分配好了空间,以后也不能再更改。比如String s = “a “ + ”b” + “c”;
这一句话其实就让内存开辟了五块空间,第一块存a,第二块存b,第三块存c,第四块存ab,第五块存abc。用StringBuilder时可以大大节省空间,append()方法使得可以在一个字符串后不断追加内容,既节省了空间又提升了效率。
String->StringBuilder,可以使用StringBuilder中的构造方法,即:
String s = “Hello”;
StringBuilder stringBuilder = new StringBuilder(s);
stringBuilder.append(“ World”);
System.out.println(stringBuilder);
StringBuilder->String,可以使用StringBuilder类中的toString()的方法,即在以上的基础上
s = stringBuilder.toString();
System.out.println(s);
基本数据类型和包装类
int->Integer char->Character bool->Boolean float->Float double->Double long->Long
byte->Byte
自动拆箱指包装类对象无法直接进行运算,需要拆箱转为基本数据类型后参与运算JDK1.5后的版本实现了这一特性
自动装箱是指一些集合无法直接存储基本数据类型,而只能存储他们的包装类,这时需要自动装箱,将基本数据类型转换为包装类之后进行加入集合的操作。
ArrayList a = new ArrayList<>();
a.add(1); // 自动装箱,因为1是基本数据类型int,Java自动将其转为包装类Integer后才// 能装入集合
int temp = a.get(0); // 自动拆箱,因为a.get(0)返回的是包装类,需转换为基本数据类型int后才能赋值给temp。即自动实现了a.get(0).intValue()功能。
字符串常量池:程序中直接写上双引号的字符串,就在常量池中,对于基本数据类型来说,==判断的是数值是否相等。而对于引用类型来说,==判断的是地址值是否相等。
字符串常量池是建立在堆当中的,如String s1 = “abc”;此时JVM会为其分配出字符串常量池的一小块空间,并将s1指向该地址,若紧接着有语句String s2 = “abc”;此时不会再单独分配空间,因为常量池中已经有abc,所以让s2直接指向该常量池的地址即可

标题234

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值