Java⾯试题马⼠兵
b站题⽬和答案
Java⾯向对象有哪些特征,如何应⽤
⾯向对象编程是利⽤类和对象编程的⼀种思想。万物可归类,类是对于世界事物的⾼度抽象 ,不同的事物之间有不同的关系 ,⼀个类⾃⾝与外界的封装关系,⼀个⽗类和⼦类的继承关系, ⼀个类和多个类的多态关系。万物皆对象,对象是具体的世界事物,⾯向对象的三⼤特征封装,继承,多态。封装,封装说明⼀个类⾏为和属性与其他类的关系,低耦合,⾼内聚;继承是⽗类和⼦类的关系,多态说的是类与类的关系。
封装隐藏了类的内部实现机制,可以在不影响使⽤的情况下改变类的内部结构,同时也保护了数据。对外界⽽已它的内部细节是隐藏的,暴露给外界的只是它的访问⽅法。属性的封装:使⽤者只能通过事先定制好的⽅法来访问数据,可以⽅便地加⼊逻辑控制,限制对属性的 不合理操作;⽅法的封装:使⽤者按照既定的⽅式调⽤⽅法,不必关⼼⽅法的内部实现,便于使⽤; 便于修改,增强 代码的可维护性;
继承是从已有的类中派⽣出新的类,新的类能吸收已有类的数据属性和⾏为,并能扩展新的能⼒。在本质上是特殊~⼀般的关系,即常说的is-a关系。⼦类继承⽗类,表明⼦类是⼀种特殊的⽗类,并且具有⽗
类所不具有的⼀些属性或⽅法。从多种实现类中抽象出⼀个基类,使其具备多种实现类的共同特性 ,当实现类⽤extends关键字继承了基类(⽗类)后,实现类就具备了这些相同的属性。继承的类叫做⼦类(派⽣类或者超类),被继承的类叫做⽗类(或者基类)。⽐如从猫类、狗类、虎类中可以抽象出⼀个动物类,具有和猫、狗、虎类的共同特性(吃、跑、叫等)。Java通过extends关键字来实现继承,⽗类中通过private定义的变量和⽅法不会被继承,不能在⼦类中直接操作⽗类通过private定义的变量以及⽅法。继承避免了对⼀般类和特殊类之间共同特征进⾏的重复描述,通过继承可以清晰地表达每⼀项共同特征所适应的概念范围,在⼀般类中定义的属性和操作适应于这个类本⾝以及它以下的每⼀层特殊类的全部对象。运⽤继承原则使得系统模型⽐较简练也⽐较清晰。
相⽐于封装和继承,Java多态是三⼤特性中⽐较难的⼀个,封装和继承最后归结于多态, 多态指的是类和类的关系,两个类由继承关系,存在有⽅法的重写,故⽽可以在调⽤时有⽗类引⽤指向⼦类对象。多态必备三个要素:继承,重写,⽗类引⽤指向⼦类对象。
HashMap原理是什么,在jdk1.7和1.8中有什么区别
HashMap 根据键的 hashCode 值存储数据,⼤多数情况下可以直接定位到它的值,因⽽具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许⼀条记录的键为null,允许多条记录的值为 null。HashMap ⾮线程安全,即任⼀时刻可以有多个线程同时写 HashMap,可能会导致数据的不⼀致。
如果需要满⾜线程安全,可以⽤Collections 的 synchronizedMap ⽅法使 HashMap 具有线程安全的能⼒,或者使⽤ ConcurrentHashMap。我们⽤下⾯这张图来介绍
HashMap 的结构。
JAVA7 实现
⼤⽅向上,HashMap ⾥⾯是⼀个数组,然后数组中每个元素是⼀个单向链表。上图中,每个绿⾊
的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和⽤于单向链表的 next。
1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组⼤⼩为当前的 2 倍。
2. loadFactor:负载因⼦,默认为 0.75。
3. threshold:扩容的阈值,等于 capacity * loadFactor
**JAVA8实现 **
Java8 对 HashMap 进⾏了⼀些修改,最⼤的不同就是利⽤了红⿊树,所以其由 数组+链表+红⿊树 组成。
根据 Java7 HashMap 的介绍,我们知道,查的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表⼀个个⽐较下去才能到我们需要的,时间复杂度取决
于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红⿊树,在这些位置进⾏查的时候可以降低时间复杂度为O(logN)。
ArrayList和LinkedList有什么区别
ArrayList和LinkedList都实现了List接⼝,他们有以下的不同点:
ArrayList是基于索引的数据接⼝,它的底层是数组。它可以以O(1)时间复杂度对元素进⾏随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每⼀个元素都和它的前⼀个和后⼀个元素链接在⼀起,在这种情况下,查某个元素的时间复杂度是O(n)。
相对于ArrayList,LinkedList的插⼊,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算⼤⼩或者是更新索引。
LinkedList⽐ArrayList更占内存,因为LinkedList为每⼀个节点存储了两个引⽤,⼀个指向前⼀个元素,⼀个指向下⼀个元素。
也可以参考ArrayList vs. LinkedList。
1. 因为 Array 是基于索引 (index) 的数据结构,它使⽤索引在数组中搜索和读取数据是很快的。 Array 获取数据的时间复杂度是 O(1), 但是要删除数据却是开销很⼤的,因
为这需要重排数组中的所有数据。
2. 相对于 ArrayList , LinkedList 插⼊是更快的。因为 LinkedList 不像 ArrayList ⼀样,不需要改变数组的⼤⼩,也不需要在数组装满的时候要将所有的数据重新装⼊⼀
个新的数组,这是 ArrayList 最坏的⼀种情况,时间复杂度是 O(n) ,⽽ LinkedList 中插⼊或删除的时间复杂度仅为 O(1) 。 ArrayList 在插⼊数据时还需要更新索引(除了插⼊数组的尾部)。
3. 类似于插⼊数据,删除数据时, LinkedList 也优于 ArrayList 。
4. LinkedList 需要更多的内存,因为 ArrayList 的每个索引的位置是实际的数据,⽽ LinkedList 中的每个节点中存储的是实际的数据和前后节点的位置 ( ⼀个 LinkedList
实例存储了两个值: Node first 和 Node last 分别表⽰链表的其实节点和尾节点,每个 Node 实例存储了三个值: E item,Node next,Node pre) 。
什么场景下更适宜使⽤ LinkedList,⽽不⽤ArrayList
1. 你的应⽤不会随机访问数据 。因为如果你需要LinkedList中的第n个元素的时候,你需要从第⼀个元素顺序数到第n个数据,然后读取数据。
2. 你的应⽤更多的插⼊和删除元素,更少的读取数据 。因为插⼊和删除元素不涉及重排数据,所以它要⽐ArrayList要快。
换句话说,ArrayList的实现⽤的是数组,LinkedList是基于链表,ArrayList适合查,LinkedList适合增删
以上就是关于 ArrayList和LinkedList的差别。你需要⼀个不同步的基于索引的数据访问时,请尽量使⽤ArrayList。ArrayList很快,也很容易使⽤。但是要记得要给定⼀个合适的初始⼤⼩,尽可能的减少更改数组的⼤⼩。
⾼并发中的集合有哪些问题
**第⼀代线程安全集合类 **
Vector、Hashtable
是怎么保证线程安排的: 使⽤synchronized修饰⽅法*
缺点:效率低下
第⼆代线程⾮安全集合类
ArrayList、HashMap
线程不安全,但是性能好,⽤来替代Vector、Hashtable
使⽤ArrayList、HashMap,需要线程安全怎么办呢?
使⽤ Collections.synchronizedList(list); Collections.synchronizedMap(m);
底层使⽤synchronized代码块锁 虽然也是锁住了所有的代码,但是锁在⽅法⾥边,并所在⽅法外边性能可以理解为稍有提⾼吧。毕竟进⽅法本⾝就要分配资源的
第三代线程安全集合类
在⼤量并发情况下如何提⾼集合的效率和安全呢?
urrent.*
ConcurrentHashMap:
CopyOnWriteArrayList :
CopyOnWriteArraySet: 注意 不是CopyOnWriteHashSet*
底层⼤都采⽤Lock锁(1.8的ConcurrentHashMap不使⽤Lock锁),保证安全的同时,性能也很⾼。
jdk1.8的新特性有哪些
⼀、接⼝的默认⽅法
Java 8允许我们给接⼝添加⼀个⾮抽象的⽅法实现,只需要使⽤ default关键字即可,这个特征⼜叫做扩展⽅法,⽰例如下:
代码如下:
interface Formula { double calculate(int a);
default double sqrt(int a) { return Math.sqrt(a); } }
Formula接⼝在拥有calculate⽅法之外同时还定义了sqrt⽅法,实现了Formula接⼝的⼦类只需要实现⼀个calculate⽅法,默认⽅法sqrt将在⼦类上可以直接使⽤。
代码如下:
Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } };
formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0
⽂中的formula被实现为⼀个匿名类的实例,该代码⾮常容易理解,6⾏代码实现了计算 sqrt(a * 100)。在下⼀节中,我们将会看到实现单⽅法接⼝的更简单的做法。
译者注: 在Java中只有单继承,如果要让⼀个类赋予新的特性,通常是使⽤接⼝来实现,在C++中⽀持多继承,允许⼀个⼦类同时具有多个⽗类的接⼝与功能,在其他语⾔中,让⼀个类同时具有其他的可复⽤代码的⽅法叫做mixin。新的Java 8 的这个特新在编译器实现的⾓度上来说更加接近Scala的trait。 在C#中也有名为扩展⽅法的概念,允许给已存在的类型扩展⽅法,和Java 8的这个在语义上有差别。
⼆、L am bda 表达式
⾸先看看在⽼版本的Java中是如何排列字符串的:
代码如下:
List names = Arrays.asList("peterF", "anna", "mike", "xenia");
Collections.sort(names, new Comparator() { @Override public int compare(String a, String b) { return bpareTo(a); } });
只需要给静态⽅法 Collections.sort 传⼊⼀个List对象以及⼀个⽐较器来按指定顺序排列。通常做法都是创建⼀个匿名的⽐较器对象然后将其传递给sort⽅法。
在Java 8 中你就没必要使⽤这种传统的匿名对象的⽅式了,Java 8提供了更简洁的语法,lambda表达式:
代码如下:
Collections.sort(names, (String a, String b) -> { return bpareTo(a); });
看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:
代码如下:
Collections.sort(names, (String a, String b) -> bpareTo(a));
对于函数体只有⼀⾏代码的,你可以去掉⼤括号{}以及return关键字,但是你还可以写得更短点:
代码如下:
Collections.sort(names, (a, b) -> bpareTo(a));
Java编译器可以⾃动推导出参数类型,所以你可以不⽤再写⼀次类型。接下来我们看看lambda表达式还能作出什么更⽅便的东西来:
三、函数式接⼝
Lambda表达式是如何在java的类型系统中表⽰的呢?每⼀个lambda表达式都对应⼀个类型,通常是接⼝类型。⽽“函数式接⼝”是指仅仅只包含⼀个抽象⽅法的接⼝,每⼀个该类型的lambda表达式都会被匹配到这个抽象⽅法。因为 默认⽅法 不算抽象⽅法,所以你也可以给你的函数式接⼝添加默认⽅法。
我们可以将lambda表达式当作任意只包含⼀个抽象⽅法的接⼝类型,确保你的接⼝⼀定达到这个要求,你只需要给你的接⼝添加 @FunctionalInterface 注解,编译器如果发现你标注了这个注解的接⼝有多于⼀个抽象⽅法的时候会报错的。
⽰例如下:
代码如下:
@FunctionalInterface interface Converter<F, T> { T convert(F from); } Converter<String, Integer> converter = (from) -> Integer.valueOf(from); Integer converted = vert("123"); System.out.println(converted); // 123
需要注意如果@FunctionalInterface如果没有指定,上⾯的代码也是对的。
译者注 将lambda表达式映射到⼀个单⽅法的接⼝上,这种做法在Java 8之前就有别的语⾔实现,⽐如Rhino JavaScript解释器,如果⼀个函数参数接收⼀个单⽅法的接⼝⽽你传递的是⼀个function,Rhino 解释器会⾃动做⼀个单接⼝的实例到function的适配器,典型的应⽤场景有 org.w3c.dom.events.EventTarget 的addEventListener 第⼆个参数 EventListener。
四、⽅法与构造函数引⽤
前⼀节中的代码还可以通过静态⽅法引⽤来表⽰:
代码如下:
Converter<String, Integer> converter = Integer::valueOf; Integer converted = vert("123"); System.out.println(converted); // 123
Java 8 允许你使⽤ :: 关键字来传递⽅法或者构造函数引⽤,上⾯的代码展⽰了如何引⽤⼀个静态⽅法,我们也可以引⽤⼀个对象的⽅法:
代码如下:
converter = something::startsWith; String converted = vert("Java"); System.out.println(converted); // "J"
接下来看看构造函数是如何使⽤::关键字来引⽤的,⾸先我们定义⼀个包含多个构造函数的简单类:
代码如下:
class Person { String firstName; String lastName;
Person() {}
Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
接下来我们指定⼀个⽤来创建Person对象的对象⼯⼚接⼝:
代码如下:
interface PersonFactory
{ P create(String firstName, String lastName); }
这⾥我们使⽤构造函数引⽤来将他们关联起来,⽽不是实现⼀个完整的⼯⼚:
代码如下:
PersonFactory personFactory = Person::new; Person person = ate("Peter", "Parker");
我们只需要使⽤ Person::new 来获取Person类构造函数的引⽤,Java编译器会⾃动根据ate⽅法的签名来选择合适的构造函数。
五、L am bda 作⽤域
在lambda表达式中访问外层作⽤域和⽼版本的匿名对象中的⽅式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。
六、访问局部变量
我们可以直接在lambda表达式中访问外层的局部变量:
代码如下:
final int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
但是和匿名对象不同的是,这⾥的变量num可以不⽤声明为final,该代码同样正确:
代码如下:
int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
不过这⾥的num必须不可被后⾯的代码修改(即隐性的具有final的语义),例如下⾯的就⽆法编译:
代码如下:
int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); num = 3;
在lambda表达式中试图修改num同样是不允许的。
七、访问对象字段与静态变量
和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读⼜可写。该⾏为和匿名对象是⼀致的:
代码如下:
class Lambda4 { static int outerStaticNum; int outerNum;
void testScopes() { Converter<Integer, String> stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); };
Converter<Integer, String> stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
⼋、访问接⼝的默认⽅法
java面向对象的特征有哪些方面还记得第⼀节中的formula例⼦么,接⼝Formula定义了⼀个默认⽅法sqrt可以直接被formula的实例包括匿名对象访问到,但是在lambda表达式中这个是不⾏的。 Lambda 表达式中是⽆法访问到默认⽅法的,以下代码将⽆法编译:
代码如下:
Formula formula = (a) -> sqrt( a * 100); Built-in Functional Interfaces
JDK 1.8 API包含了很多内建的函数式接⼝,在⽼Java中常⽤到的⽐如Comparator或者Runnable接⼝,这些接⼝都增加了@FunctionalInterface注解以便能⽤在lambda 上。 Java 8 API同样还提供了很多全新的函数式接⼝来让⼯作更加⽅便,有⼀些接⼝是来⾃Google Guava库⾥的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使⽤的。
Predicate****接⼝
Predicate 接⼝只有⼀个参数,返回boolean类型。该接⼝包含多种默认⽅法来将Predicate组合成其他复杂的逻辑(⽐如:与,或,⾮):
代码如下:
Predicate predicate = (s) -> s.length() > 0;
Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull;
Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = ate();
Function 接⼝
Function 接⼝有⼀个参数并且返回⼀个结果,并附带了⼀些可以和其他函数组合的默认⽅法(compose, andThen):
代码如下:
Function<String, Integer> toInteger = Integer::valueOf; Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
Supplier 接⼝ Supplier 接⼝返回⼀个任意范型的值,和Function接⼝不同的是该接⼝没有任何参数
代码如下:
Supplier personSupplier = Person::new; (); // new Person
Consumer 接⼝ Consumer 接⼝表⽰执⾏在单个参数上的操作。
代码如下:
Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Lu
ke", "Skywalker"));
Comparator 接⼝ Comparator 是⽼Java中的经典接⼝, Java 8在此之上添加了多种默认⽅法:
代码如下:
Comparator comparator = (p1, p2) -> p1.firstNamepareTo(p2.firstName);
Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland");
comparatorpare(p1, p2); // > versed()pare(p1, p2); // < 0
Optional 接⼝
Optional 不是函数是接⼝,这是个⽤来防⽌NullPointerException异常的辅助类型,这是下⼀届中将要⽤到的重要概念,现在先简单的看看这个接⼝能⼲什么:
Optional 被定义为⼀个简单的容器,其值可能是null或者不是null。在Java 8之前⼀般某个函数应该返回⾮空对象但是偶尔却可能返回了null,⽽在Java 8中,不推荐你返回null⽽是返回Optional。
代码如下:
Optional optional = Optional.of("bam");
optional.isPresent(); // (); // "bam" Else("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Stream 接⼝
java.util.Stream 表⽰能应⽤在⼀组元素上⼀次执⾏的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回⼀特定类型的计算结果,⽽中间操作返回Stream 本⾝,这样你就可以将多个操作依次串起来。Stream 的创建需要指定⼀个数据源,⽐如 java.util.Collection的⼦类,List或者Set, Map不⽀持。Stream的操作可以串⾏执⾏或者并⾏执⾏。
⾸先看看Stream是怎么⽤,⾸先创建实例代码的⽤到的数据List:
代码如下:
List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Java 8扩展了集合类,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建⼀个Stream。下⾯⼏节将详细解释常⽤的Stream操作:
Filter 过滤
过滤通过⼀个predicate接⼝来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应⽤其他Stream操作(⽐如forEach)。forEach需要⼀个函数来对过滤后的元素依次执⾏。forEach是⼀个最终操作,所以我们不能在forEach之后来执⾏其他Stream操作。
代码如下:
stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println);
// "aaa2", "aaa1"
Sort 排序
排序是⼀个中间操作,返回的是排序好后的Stream。如果你不指定⼀个⾃定义的Comparator则会使⽤默认排序。
代码如下:
stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println);
// "aaa1", "aaa2"
需要注意的是,排序只创建了⼀个排列好后的Stream,⽽不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:
代码如下:
System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map 映射 中间操作map会将元素根据指定的Function接⼝来依次将元素转成另外的对象,下⾯的⽰例展⽰了将字符串转换为⼤写字符串。你也可以通过map来讲对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。
代码如下:
stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> bpareTo(a)) .forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match 匹配
Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回⼀个boolean类型的值。
代码如下:
boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
Count 计数 计数是⼀个最终操作,返回Stream中元素的个数,返回值类型是long。
代码如下:
long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count();
System.out.println(startsWithB); // 3
Reduce 规约
这是⼀个最终操作,允许通过指定的函数来讲stream中的多个元素规约为⼀个元素,规越后的结果是通过Optional接⼝表⽰的:
代码如下:
Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
并⾏****Streams
前⾯提到过Stream有串⾏和并⾏两种,串⾏Stream上的操作是在⼀个线程中依次完成,⽽并⾏Stream则是在多个线程上同时执⾏。
下⾯的例⼦展⽰了是如何通过并⾏Stream来提升性能:
⾸先我们创建⼀个没有重复元素的⼤表:
代码如下:
int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.String()); }
然后我们计算⼀下排序这个Stream要耗时多久, 串⾏排序:
代码如下:
long t0 = System.nanoTime();
long count = values.stream().sorted().count(); System.out.println(count);
long t1 = System.nanoTime();
long millis = Millis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis));
// 串⾏耗时: 899 ms 并⾏排序:
代码如下:
long t0 = System.nanoTime();
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论