分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
分类
通常用于将一个功能强大臃肿的类,分布在几个不同的文件。
只能增加方法,不能增加成员(实例)变量。
1 | Category 是表示一个指向分类的结构体的指针,其定义如下: |
可以通过runtime
实现增加属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static NSString *skillKey = @"skillKey";
@implementation People (Work)
- (void)setSkill:(NSString *)skill {
objc_setAssociatedObject(self, &skillKey, skill, OBJC_ASSOCIATION_COPY);
}
- (NSString *)skill {
return objc_getAssociatedObject(self, &skillKey);
}
@end
以上方法仅仅增加了get set方法 成员变量
_skill
并没有增加直接调用会报错
扩展
匿名的分类,不仅可以增加方法,也能增加私有属性。
没有独立的implementation
部分。
定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。
讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
atomic 的作用是在set get方法中添加spinlock_t(自旋锁)来保证数据仅被一个线程占用。
线程安全是指在多线程环境不出现意想不到的情况,仅靠atomic是无法实现的。
例如:
- A线程对属性加1,本想立马读取属性,但B线程先对属性减1,导致A线程后来读取到的值错误。
- 属性如果是集合类型,addObject等方法并不是线程安全的,属性是类的话也同理。
本质上是atomic是保证了属性的地址值的线程安全,并不是对象层面的线程安全。
被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
weak
作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。
通过调用storeWeak
来添加弱引用。
内部主要如果weak指针之前指向了一个弱引用,则用weak_unregister_no_lock
将旧的weak指针地址移除。
接着用weak_register_no_lock
把新的weak指针地址添加到弱引用表中。
如果对象没有再析构且可以被weak引用,则调用
weak_entry_for_referent
方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer
方法向其中插入weak指针地址。否则新建一个weak_entry。
对象释放时,调用rootDealloc
进行释放。
1 | inline void |
如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced
、没有关联对象!isa.has_assoc
、没有自定义的C++析构方法!isa.has_cxx_dtor
、没有用到SideTable来引用计数!isa.has_sidetable_rc
则直接快速释放。
否则调用object_dispose
1 | void *objc_destructInstance(id obj) |
调用clearDeallocating
函数根据对象地址获取weak指针地址的数组,遍历这个数组把其中的数据设为nil,最后把这个entry
从weak表中删除,最后清除这个对象数据。
SideTable
1 | struct SideTable { |
内部主要有三部分:自旋锁,引用计数表,weak表。
weak_table_t
1 | struct weak_table_t { |
内部主要包括:
weak_entries:hash数组,存储着弱引用对象的相关信息weak_entry_t
num_entries:hash数组中的元素个数
weak_entry_t
1 |
|
内部有定长数组inline_referrers[WEAK_INLINE_COUNT]
和动态数组weak_referrer_t *referrers
来存储弱引用对象的地址。通过out_of_line()
这样的函数来判断该用哪种存储方式。
关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
应用
- 添加公共属性,为分类动态的添加set get方法
- 为系统类添加方法,例如为UIView添加点击事件
- 使用KVO时关联对象作为观察者,避免自身观察自身
系统通过AssociationsManager
管理一个全局哈希表AssociationsHashMap
,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter
传入的参数协议,来管理对象的生命周期。
释放
对象释放时,会调用_object_remove_assocations
方法自动释放我们的关联对象。
KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
底层实现
执行addObserver
后runtime会创建一个这个类的子类,并将指针指向这个子类,这个子类的set方法会调用Fundation框架中的C语言函数_NSsetIntValueAndNotify(根据类型不同调用不同函数)。其中这个C语言函数就相当于调用willChangeValueForKey
,接着调用父类的set方法,最后调用didChangeValueForKey
。其实didChangeValueForKey
中会调用observeValueForKeyPath
。
取消默认KVO增加判断条件
仅需要在对象中重写didChangeValueForKye
方法,增加判断,当满足条件才调用[super didChangeValueForKey:key];
AutoreleasePool
所使用的数据结构是什么?AutoreleasePoolPage
结构体了解么?
AutoreleasePool
所使用的数据结构__AtAutoreleasePool,是由若干个AutoreleasePoolPage
以双向链表的形式存在的。
AutoreleasePoolPage
1 | class AutoreleasePoolPage |
每个AutoreleasePoolPage对象占有4096字节内存,其中56个字节用来存储它内部的成员变量,其余的用于存储autorelease对象的地址。
- 每当创建一个自动释放池时,会调用push()方法,将一个
POOL_BOUNDARY
入栈,并返回其存放的内存地址。 - 当往自动释放池添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个
POOL_BOUNDARY
。 - 当销毁一个自动释放池时,会调用pop()方法,并传入一个
POOL_BOUNDARY
,会从自动释放池中最后一个对象开始释放,直到遇到这个POOL_BOUNDARY
。
讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
对象的结构是objc_object
,类对象和元类的结构是objc_class
。
对象的isa指针指向对应的类对象,类对象的isa指针指向元类。元类的isa指针都指向根元类, 其中根元类的isa指针指向自己。
类对象的superClass指针指向其父类,其中NSObject的superClass指针为nil,根元类的superClass指针指向NSObject类对象。
同个类的对象方法都是一样的,如果在每个对象中存放一份,则会产生浪费,而保存在类对象中,就能做到同个类的对象方法只存放一份。同样类方法则存放在元类中。
class_ro_t
和 class_rw_t
的区别?
1 | struct class_ro_t { |
1 | struct class_rw_t { |
每个类都对应一个class_ro_t和class_rw_t。在编译期间,确定class_ro_t的内容。在runtime的realizeClass方法运行时,生成class_rw_t,class_rw_t包括class_ro_t,并更新data部分。实际访问类的内容时,是访问class_rw_t中的内容。
iOS 中内省的几个方法?class
方法和objc_getClass
方法有什么区别?
1 | -(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例 |
objc_getClass(obj)
方法返回的是isa指针。
[objc class]
要两类情况
当objc为实例对象时,class是实例方法,返回的是objc对象的isa指针,也就是类对象。
当objc为类对象(包括元类,根类,以及根元类)时,class是类方法,返回的是它本身。
在运行时创建类的方法objc_allocateClassPair
的方法名尾部为什么是pair(成对的意思)?
因为要创建类和元类,所以是成对的。
一个int变量被__block
修饰与否的区别?
默认为值捕获,加__block后生成结构体,复制int的引用地址。
为什么在block外部使用__weak
修饰的同时需要在内部使用__strong
修饰?
首先weak是用于解决循环引用的问题的。同时因为weak的存在,可能会发生self在block执行的过程中被置为nil,这时就需要strong来将self保持住,避免发生意想不到的事。
RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
一般来说线程在执行完任务后,就会退出。如果需要线程能够一直响应,则需要在内部添加类似循环的概念,类似EvenLoop,在没有处理消息的时候线程进行休眠,等消息来了立刻唤醒响应消息。
在iOS中这个循环就是RunLoop。主要的作用是:
- 保持app的持续运行,在main方法中会给主线程创建一个RunLoop,保证线程不被销毁
- AutoreleasePool,系统在主线程的RunLoop中注册了两个Observer。第一个Observer监听即将进入RunLoop,调用
_objc_autoreleasePoolPush
创建自动释放池。第二个Observer监听了两个事件,进入休眠之前会先释放自动释放池,然后创建一个自动释放池,在即将退出时,会释放自动释放池。 - 检测卡顿,通过监听主线程的RunLoop的状态来判断是否会出现卡顿
哪些场景可以触发离屏渲染?(知道多少说多少)
离屏渲染:因为一些限制,无法直接将渲染结果写入帧缓冲区,而需要新开辟一个缓冲区来进行渲染。
- 为contents设置了内容,无论是图片,绘制内容,有图像信息的子视图等,再加上圆角+裁剪
- 采用了光栅化的 layer (layer.shouldRasterize)
- 使用了 mask 的 layer (layer.mask)
- 需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
- 使用了高斯模糊
- 添加了投影的 layer (layer.shadow*)
- 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
了解编译的过程么?分为哪几个步骤?
- 预处理阶段
- 词法分析 输出Token流
- 语法分析 输出抽象树AST
- codegen生成IR中间码
- Optimize - 优化IR
- LLVM Bitcode - 生成字节码
- 生成汇编
- Link生成目标文件并执行
Xcode编译的过程
- 优化编译Cocoapods里面的所有依赖文件
- 编译信息写入辅助信息,创建编译后的文件架构
- 处理打包信息,例如development环境下处理xxxx.entitlements的打包信息
- 执行cocopods编译前脚本 checkPods Manifest.lock
- 编译包内所有m文件 (使用Compilec和Clang的几个主要命令)
- 链接需要的framework,例如AFNetworking.framework,Masonry.framework等信息
- 编译xib文件
- copy Xib文件,图片等资源文件放到结果目录
- 编译imageAsserts
- 处理infoplis
- 执行Cocoapods脚本
- copy标准库
- 创建.app文件和签名
App启动的过程
- 解析info.plist文件
- 创建沙盒
- 根据info.plist检查各权限状态
- 加载Mach-O文件读取dyld路径并运行dyld动态连接器(内核加载主程序,dyld只负责动态库的加载)
- dyld寻找合适的CPU运行环境
- 加载程序所需要的依赖库和我们的.h .m文件编译成的.o可执行文件,并对这些库进行链接
- 加载所有方法(runtime完成初始化)
- 加载C函数
- 加载category扩展(runtime对类结构进行初始化)
- 加载C++静态函数 加载OC的+load方法
- dyld返回main函数地址 执行main函数
静态链接了解么?静态库和动态库的区别?
静态链接和动态链接都是把程序分割成一个个独立的模块,但是静态链接是运行前就用ld链接器链接成一个完整的程序;动态链接是程序主模块被加载时候,对应的Mach-O文件里有dyld加载命令,通过这个dyld然后去找依赖的dylib(Mach-O有动态链接库加载命令),把dylib加载到内存(如果对应的dylib不在内存),然后将程序中所有未决议的符号绑定到相应的 dylib中,并进行重定位工作。
静态库:完整复制进可执行的二进制文件中
动态库:在程序冷启动时才会被链接到手机内存或App内存中
内存的几大区域,各自的职能分别是什么?
内存主要分为栈区、堆区、全局区、文字常量区、代码区等五大区域。
栈区(stack)由编译器自动分配并释放,存放的是函数的参数值,局部变量等,方法调用的实参也是保存在栈区的。栈是系统数据结构,对应线程/进程是唯一的。主要存放一些基本类型的变量和对象引用类型。
堆区(heap)由程序员分配和释放,如果程序员不释放,可能会出现内存泄露,程序结束的时候,可能会由操作系统回收。堆空间的分配总是动态的,不同堆分配的内存无法互相操作。虽然程序结束的时候所有的数据空间都会被释放回系统,但是精确的申请内存,释放内存匹配是良好程序的基本要素。主要存放用new构造的对象和数组。
全局区(静态区) (static) 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放。
文字常量区 存放常量字符串,程序结束后由系统释放;
代码区 存放函数的二进制代码
static和const有什么区别?
const
- 被const关键字修饰的实例变量,在初始化之后,其值就不能改变了
- 队指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或者二者同时指定为const;
- 对于类的成员函数,若指定其为const类型, 则表明他是一个函数,不能修改类的成员变量
static
- 在函数体内定义的static他的作用域为该函数体,该变量在内存中只被分配一次,因此,其值在下次调用时仍维持上次的值不变
- 在模块内的static全局变量可以被模块内所用函数访问,但是不能被模块外的其他函数访问
- .在模块内的staic全局变量可以被这一模块内的其他函数调用,这个函数的使用范围被限制在这个模块内;
- 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝,也就是说只要是该类的对象,那么该对象的中被static修饰的实例变量都指向同一块地址
了解内联函数么?
1 | static inline CGFloat CGFloatFromPixel(CGFloat value) { |
用于取代宏,避免了普通函数汇编时必须调用call的缺点,取消了函数的参数压栈,避免了宏的缺点,需要预编译。
什么时候会出现死锁?如何避免?
发生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
说一说你对线程安全的理解?
多线程操作共享数据不会出现想不到的结果就是线程安全的。
列举你知道的线程同步策略?
线程同步的策略有:原子操作,信号量,GCD串行队列,锁。
有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说
OSSpinLock - 自旋锁
自旋锁是一种特殊互斥锁,当一个线程需要获取自旋锁时,如果该锁已经被其他线程占用,那么会一直去请求锁,进入
忙等(busy-waiting)
状态,所以会一直占用 CPU由于自旋锁在等待锁的时候线程一直处于忙等状态,而不用进入睡眠,所以不用进行上下文切换,自旋锁的效率远高于互斥锁
自旋锁适用于
- 预计线程等待锁的时间很短
- 临界区经常访问,但竞争情况很少发生
- 自旋锁不安全,会出现优先级反转问题:如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于
忙等
状态从而占用大量 CPU 时间片。此时低优先级线程无法与高优先级线程争夺 CPU 时间片,从而导致完成任务而无法释放锁 - 在 iOS 10 及以上被废弃
1 |
|
os_unfair_lock
os_unfair_lock
用于取代不安全的OSSpinLock
,iOS 10 开始支持,当一条线程等待锁的时候会进入睡眠,不再消耗 CPU 时间,当其他线程解锁以后,操作系统会激活线程os_unfair_lock
有单一的拥有者- 这是一种不公平锁。在公平锁中,多个线程同时竞争这个锁的时候, 会考虑公平性尽可能的让不同的线程获得锁,这样会频繁进行上下文切换,牺牲性能。而在不公平锁中,系统为了减少上下文切换,当前拥有锁的线程有可能会再次获得锁,但这样做可能会让其他线程等待更长时间,造成饥饿。
1 |
|
互斥锁
- 互斥锁是可以看作是一种特殊的信号量,当一条线程等待锁的时候会进入睡眠状态
- 互斥锁阻塞的过程分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入睡眠状态,让出时间片,此时线程就不占用 CPU 时间片,等锁可用的时候,这个线程会立即被唤醒
pthread_mutex
pthread
表示 POSIX thread
,是 POSIX 标准的 unix 多线程库,定义了一组跨平台的线程相关的API。pthread_mutex
是一种用 C 语言实现的互斥锁,有单一的拥有者。
1 |
|
NSLock
NSLock
是以 Objective-C 对象的形式对pthread_mutex
的封装,属性为PTHREAD_MUTEX_ERRORCHECK
,它会损失一定性能换来错误提示NSLock
比pthread_mutex
略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响NSLock
有单一的拥有者
1 | NSLock *lock = [[NSLock alloc] init]; |
递归锁
递归锁是一种特殊互斥锁。递归锁允许单个线程在释放之前多次获取锁,其他线程保持睡眠状态,直到锁的所有者释放锁的次数与获取它的次数相同。递归锁主要在递归迭代中使用,但也可能在多个方法需要单独获取锁的情况下使用。
pthread_mutex(Recursive)
pthread_mutex
支持递归锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE
即可,它有单一的拥有者。
1 |
|
NSRecursiveLock
NSRecursiveLock
是以 Objective-C 对象的形式对 pthread_mutex(Recursive)
的封装,它有单一的拥有者。
1 | NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; |
@synchronized
@synchronized
是对pthread_mutex(Recursive)
的封装,所以它支持递归加锁- 需要传入一个 Objective-C 对象,可以理解为把这个对象当做锁来使用
- 实际上它是用
objc_sync_enter(id obj)
和objc_sync_exit(id obj)
来进行加锁和解锁 - 底层实现:在底层存在一个全局用来存放锁的哈希表(可以理解为锁池),对传入的对象地址的哈希值作为key,去查找对应的递归锁
@synchronized
额外还会设置异常处理机制,性能消耗较大@synchronized
有单一的拥有者
1 | @synchronized(lock) { |
条件锁
条件锁是一种特殊互斥锁,需要条件变量(condition variable) 来配合。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。条件锁是为了解决 生产者-消费者模型
。
pthread_mutex – 条件锁
pthread_mutex
配合 pthread_cond_t
,可以实现条件锁,其中 pthread_cond_t
没有拥有者。
1 |
|
NSCondition
NSCondition
是以 Objective-C 对象的形式对 pthread_mutex
和 pthread_cond_t
进行了封装,NSCondition
没有拥有者。
1 | NSCondition *condition = [[NSCondition alloc] init]; |
NSConditionLock
NSConditionLock
是对 NSCondition
的进一步封装,可以设置条件变量的值。通过改变条件变量的值,可以使任务之间产生依赖关系,达到使任务按照一定的顺序执行,它有单一的拥有者(不确定)。
1 | // 初始化设置条件变量的为1,如果不设置则默认为0 |
读写锁
读写锁是一种特殊互斥锁,提供”多读单写”的功能,多个线程可以同时对共享资源进行读取,但是同一时间只能有一条线程对共享资源进行写入。
pthread_rwlock
pthread_rwlock
有多个拥有者。
1 |
|
GCD 的 Barrier函数
GCD 的 Barrier 函数也可以实现”多读单写”的功能
Barrier 函数的作用是:等其他任务执行完毕,才会执行任务自己的任务;会执行完毕自己的任务,才会继续执行其他任务
这个函数传入的并发队列必须是自己通过
dispatch_queue_cretate
创建的,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于 dispatch_async 函数的效果
1 | dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT); |
总结:
OSSpinLock
和os_unfair_lock
性能很高,但是一个是已经废弃,一个是低级锁,苹果不建议使用低级锁dispatch_semaphore
和pthread_mutex
也具有不错的性能,NSLock
是pthread_mutex
的封装,性能上接近- 个人建议在 Objective-C 中直接使用面向对象的
NSLock
,而在 Swif t中使用GCD 串行队列
除了单例,观察者设计模式以外,还知道哪些设计模式?分别介绍一下
工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。
装饰器模式:不改变原有对象的前提下,动态地给一个对象增加一些额外的功能。
代理模式:为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。
命令模式:将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化,或者说如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合。
享元模式:主要用于减少同一类对象的大量创建,以减少内存占用,提高项目流畅度。在享元模式中有两个重要的概念,即内部状态和外部状态:
- 内部状态:在享元对象内部不随外界环境改变而改变的共享部分
- 外部状态:随着环境的改变而改变,不能够共享的状态就是外部状态
最喜欢哪个设计模式?为什么?
iOS中最常见用到的设计模式便是代理模式。但要说最喜欢的还是装饰器模式。整个模式非常灵活,它将各种能力抽象出来,当某个对象需要某种能力时,只需要简单集成就可以,非常清晰。
iOS SDK 里面有哪些设计模式的实践?
代理模式:delegate
工厂模式:NSNumber
1 | + (NSNumber *)numberWithInt:(int)value; |
装饰器模式:Catagory
观察者模式:KVO
单例模式:dispatch_once
享元模式:UITableViewCell 重用
设计模式是为了解决什么问题的?
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码百设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被度他人理解、保证代码可靠性。
设计模式最主要解决的问题是通过封装和隔离变化点来处理软件的各种变化问题。
隔离变化的好处在于,将问系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。很多设计模答式的意图中都明显地版指出了其对问题的解决方案,学习设计模式的要点是发现其解决方案中封装的变化点。
MVC和MVVM的区别?MVVM和MVP的区别?
MVC主要的贡献在于对软件整体进行分层。主要分为三层,Model,View,Controller。View直接通过读取Model进行显示。
MVP由MVC演化过来,相通的地方Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。主要的区别在于View不直接使用Model,它们之间通信是通过Presenter进行的。
MVVM将View的状态和行为抽象化,
面向对象的几个设计原则了解么?最好可以结合场景来说。
开闭原则(Open Close Principle):个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
- 用抽象构建框架,用实现扩展细节。
- 不以改动原有类的方式来实现新需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。
单一职责原则(Single Responsibility Principle):一个类只允许有一个职责,即只有一个导致该类变更的原因。
依赖倒置原则(Dependency Inversion Principle)
- 依赖抽象,而不是依赖实现。
- 抽象不应该依赖细节;细节应该依赖抽象。
- 高层模块不能依赖低层模块,二者都应该依赖抽象。
接口分离原则(Interface Segregation Principle):多个特定的客户端接口要好于一个通用性的总接口。
- 客户端不应该依赖它不需要实现的接口。
- 不建立庞大臃肿的接口,应尽量细化接口,接口中的方法应该尽量少。
迪米特法则(Law of Demeter):一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。
- 一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。
里氏替换原则(Liskov Substitution Principle):所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。
在继承体系中,子类中可以增加自己特有的方法,也可以实现父类的抽象方法,但是不能重写父类的非抽象方法,否则该继承关系就不是一个正确的继承关系。
可以说几个重构的技巧么?你觉得重构适合什么时候来做?
你觉得框架和设计模式的区别是什么?
看过哪些第三方框架的源码,它们是怎么设计的?设计好的地方在哪里,不好的地方在哪里,如何改进?
链表和数组的区别是什么?插入和查询的时间复杂度分别是多少?
链表
- 可以在内存的任何地方,不要求连续
- 每个数据中保存了下一个数据的内存地址
- 插入快 O(1) 查询慢O(n) 删除O(1)
数组
- 是一块连续的内存
- 需要预留空间,使用前需要申请空间
- 插入慢O(n) 查询快O(1) 删除O(1)
哈希表是如何实现的?如何解决地址冲突?
哈希表(hash table
,也叫散列表),是根据键(key
)直接访问访问在内存储存位置的数据结构。 哈希表本质是一个数组,数组中的每一个元素成为一个箱子,箱子中存放的是键值对。根据下标index
从数组中取value
。关键是如何获取index
,这就需要一个固定的函数(哈希函数),将key
转换成index
。不论哈希函数设计的如何完美,都可能出现不同的key
经过hash
处理后得到相同的hash
值,这时候就需要处理哈希冲突。
拉链法
简单来说就是 数组 + 链表 。将键通过hash函数映射为大小为M的数组的下标索引,数组的每一个元素指向一个链表,链表中的每一个结点存储着hash出来的索引值为结点下标的键值对。
拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
开放定址线性探测发为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放定址线性探测发构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放定址线性探测发中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放定址线性探测发处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
指针需要额外的空间,故当结点规模较小时,开放定址线性探测发较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址线性探测发中的冲突,从而提高平均查找速度。
开放定址线性探测法
使用两个大小为N的数组(一个存放keys,另一个存放values)。使用数组中的空位解决碰撞,当碰撞发生时(即一个键的hash值对应数组的下标被另外一个键占用)直接将下标索引加一(index += 1
)。
容易产生堆积问题;
不适于大规模的数据存储;
- 散列函数的设计对冲突会有很大的影响;
- 插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;
- 结点规模很大时会浪费很多空间;