-
Dalvik虚拟机与java虚拟机的区别
1.java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码
;传统的Java程序经过编译,生成Java字节码保存在class文件中,java虚拟机通过解码class文件中的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中Dalvik虚拟机通过解释Dex文件来执行这些字节码。
2.Dalvik可执行文件体积更小。SDK中有一个叫dx的工具负责将java字节码转换为Dalvik字节码。
3.java虚拟机与Dalvik虚拟机架构不同。java虚拟机基于栈架构
。程序在运行时虚拟机需要频繁的从栈上读取或写入数据。这过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。Dalvik虚拟机基于寄存器架构
,数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式快的多.
-
java类加载机制
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
-
类的五个加载过程
类的加载过程包括加载,验证,准备,解析,初始化五个阶段,这里的几个阶段是按顺序开始的,而不是按顺序进行或者完成,因为这些阶段通常交叉进行的。
image.png
五个加载过程详解
-
双亲委托机制
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
image.png
双亲委派模型意义:
系统类防止内存中出现多份同样的字节码
保证Java程序安全稳定运行
-
JVM内存模型
虚拟机栈 VM Stack
image.png
本地方法栈 Native Method Stack
堆 Heap
方法区 Method Area
程序计数器
内存模型详情
-
JVM 内存区域 开线程影响哪块内存
Heap(堆) Method Area(方法区)
-
垃圾收集机制 对象创建,新生代与老年代
新生代垃圾收集机制
新生代又可分为三个部分:
- 一个Eden区
- 两个Survivor区
在三个区域中有两个是Survivor区。对象在三个区域中的存活过程如下:
- 大多数新生对象都被分配在Eden区。
- 第一次GC过后Eden中还存活的对象被移到其中一个Survivor区。
- 再次GC过程中,Eden中还存活的对象会被移到之前已移入对象的Survivor区。
- 一旦该Survivor区域无空间可用时,还存活的对象会从当前Survivor区移到另一个空的Survivor区。而当前Survivor区就会再次置为空状态。
- 经过数次在两个Survivor区域移动后还存活的对象最后会被移动到老年代。
如上所述,两个Survivor区域在任何时候必定有一个保持空白。如果同时有数据存在于两个Survivor区或者两个区域的的使用量都是0,则意味着你的系统可能出现了运行错误。
在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。
老年代垃圾收集机制
老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
- 新生代:大部分的新创建对象分配在新生代。因为大部分对象很快就会变得不可达,所以它们被分配在新生代,然后消失不再。当对象从新生代移除时,我们称之为"minor GC"。
- 老年代:存活在新生代中但未变为不可达的对象会被复制到老年代。一般来说老年代的内存空间比新生代大,所以在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,我们称之为"major GC"(或者full GC)。
-
GC算法
标记清除,标记整理,复制,分代收集
-
适配器模式,装饰者模式,外观模式的异同?
适配器模式:将原有类接口转换为目标代码需求的接口。
装饰者模式:能动态的新增或组合对象的行为
外观模式:注重多个类的集成、统一适配
-
垃圾回收机制与调用System.gc()区别
垃圾回收机制:用以跟踪正在使用的对象和发现并回收不再使用(引用)的对象。该机制可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽,以及不恰当的内存释放所造成的内存非法引用。
调用System.gc():System.gc() 的作用只是提醒虚拟机进行垃圾回收,回不回收得由虚拟机决定。
-
GC 性能调优?
GC调优的目标分为以下两类:
- 降低移动到老年代的对象数量
- 缩短Full GC的执行时间
四大引用
强,软,弱,虚,并说明下合适GC
垃圾搜集算法有哪些?G1算法?
-
LRUCache原理
LruCache是个泛型类,主要算法原理是把最近使用的对象用
强引用
(即我们平常使用的对象引用方式)存储在LinkedHashMap
中,通过内部一个双向的循环链表实现的。当缓存满时,把最近最少
使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。
Android LruCache源码分析
-
静态方法是否能被重写
静态属性和静态方法可以被继承,但是没有被重写(overwrite)而是被隐藏.
非静态方法可以被继承和重写,因此可以实现多态。
-
TCP/UDP的区别
UDP:
将数据及源和目的封装成数据包中,不需要建立连接
每个数据报的大小在限制在64k内
因无连接,是不可靠协议
不需要建立连接,速度快
TCP:
建立连接,形成传输数据的通道
在连接中进行大数据量传输
通过三次握手完成连接,是可靠协议
必须建立连接,效率会稍低
-
Socket与Http区别
Socket: Socket不属于协议范畴,而是一个调用接口(API),Socket是应用层与TCP/IP协议族通信的中间软件抽象层。它既可使用下层的 TCP,也可以使用UDP,Socket连接是长连接,理论上客户端和服务器端一旦建立连接将不会主动断开此连接。
Http: 超文本传输协议,首先它是一个协议,并且是基于TCP/IP协议基础之上的应用层协议。HTTP是基于请求-响应形式并且是短连接,并且是无状态的协议。
长连接:建立一个连接后保持一段时间,这段时间多个请求和响应使用这个连接。
短连接:每一次请求建立一个连接,等服务器响应返回就关闭连接。
-
TCP的三次握手?两次行不行?为什么?TCP攻击知道吗?如何进行攻击?
- 三次握手
(1) 客户端向服务器端发送连接请求包SYN(syn=j),等待服务器回应;
(2) 服务器端收到客户端连接请求包SYN(syn=j)后,将客户端的请求包SYN(syn=j)放入到自己的未连接队列,此时服务器需要发送两个包给客户端;
- 1.向客户端发送确认自己收到其连接请求的确认包ACK(ack=j+1),向客户端表明已知道了其连接请求
- 2.向客户端发送连接询问请求包SYN(syn=k),询问客户端是否已经准备好建立连接,进行数据通信;
(3) 客户端收到服务器的ACK(ack=j+1)和SYN(syn=k)包后,知道了服务器同意建立连接,此时需要发送连接已建立的消息给服务器;向服务器发送连接建立的确认包ACK(ack=k+1),回应服务器的SYN(syn=k)告诉服务器,我们之间已经建立了连接,可以进行数据通信。
- 为什么不能只两次握手?
有了三次握手的详细步骤,就可以分析为什么需要三次握手而不是两次握手了。三次握手的目的:
消除旧有连接请求的SYN消息对新连接的干扰,同步连接双方的序列号和确认号并交换TCP 窗口大小信息
。
设想:如果只有两次握手,那么第二次握手后服务器只向客户端发送ACK包,此时客户端与服务器端建立连接。在这种握手规则下:
假设:如果发送网络阻塞,由于TCP/IP协议定时重传机制,B向A发送了两次SYN请求,分别是x1和x2,且因为阻塞原因,导致x1连接请求和x2连接请求的TCP窗口大小和数据报文长度不一致
,如果最终x1达到A,x2丢失,此时A同B建立了x1的连接,这个时候,因为AB已经连接,B无法知道是请求x1还是请求x2同B连接,如果B默认是最近的请求x2同A建立了连接,此时B开始向A发送数据,数据报文长度为x2定义的长度,窗口大小为x2定义的大小,而A建立的连接是x1,其数据包长度大小为x1,TCP窗口大小为x1定义,这就会导致A处理数据时出错。很显然,如果A接收到B的请求后,A向B发送SYN请求y3(y3的窗口大小和数据报长度等信息为x1所定义),确认了连接建立的窗口大小和数据报长度为x1所定义,A再次确认回答建立x1连接,然后开始相互传送数据,那么就不会导致数据处理出错了。
-
Java设计模式,观察者模式
-
Java中String的了解
1、String类时final类,所以是不可继承的;
2、String类是的本质是字符数组char[];
3、Java运行时会维护一个String Pool(String池),JavaDoc翻译很模糊“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象仅仅存在于方法的堆栈区。
-
抽象类与接口的区别
抽象类:
1.抽象类不能创建对象,因为其中包含了未实现的抽象方法
2.继承抽象类的子类,如果没有实现抽象方法,则这个类也是抽象类
3.如果使用抽象类,必须使用子类来实现并覆写抽象类中所有抽象方法
4.abstract类中可以有abstract方法,也可以有非abstract方法
接口
1.接口体的声明包含`常量的声明(没有变量)和抽象方法.接口体中只有抽象方法,没有普通的方法,而且接口体中所有的常量的访问权限一定是public,而且是static常量,所有的抽象方法的访问权限一定都是public
2.接口可以实现多继承
3.接口主要用于被实现,接口中的所有方法,在子类中必须全部实现
-
静态属性和静态方法是否可以被继承?是否可以被重写?原因
Java中静态属性和静态方法可以被继承,但是没有被重写(overwrite)而是被隐藏。
原因:
- 静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成,不需要继承机制及可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为"隐藏"。如果你想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成,至于是否继承一说,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在"隐藏"的这种情况。
- 多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键)。有了继承和重写就可以实现父类的引用指向不同子类的对象。重写的功能是:"重写"后子类的优先级要高于父类的优先级,但是“隐藏”是没有这个优先级之分的。
- 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态方法可以被继承和重写,因此可以实现多态。
-
Object 类
所有的类直接或者间接的继承了Object类
方法:
String toString()
boolean equals(Object obj)
int hashCode();//返回对象的哈希码值,标示对象的唯一性
-
String buffer 与string builder 的区别?
String类是不可变类,任何对String的改变都会引发新的String对象的生成;StringBuffer是可变类,任何对它所指代的字符串的改变都不会产生新的对象,支持并发操作,线性安全的,适合多线程中使用
StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
String,StringBuffer与StringBuilder的区别
-
内部类方面的知识
image.png静态内部类、局部内部类、匿名内部类、成员内部类
1.内部类的外嵌类的成员变量在内部类中仍然有效,内部类中的方法也可以调用外嵌类中的方法
2.内部类的类体中不可以声明类变量和类方法,外嵌类的类体中可以用内部类声明对象,作为外嵌类的成员
3.内部类仅供它的外嵌类使用,其他类不可以用某个类的内部类声明对象。
4.局部内部类中,如果要访问局部变量时,则局部变量需要final修饰
-
List,Set,Map的区别
List:存储对象是有序的,可以重复的
ArrayList:使用的数据结构是数组,线程不安全,查找速度快,增删速度慢
LinkedList:使用的数据结构是链表,线程不安全,查找速度慢,增删速度快
Vector:使用的数据结构是数组,线程安全,查找速度快,增删速度慢,被ArrayList替代
Set:存储对象是无序的,不可以重复的
HashSet:底层使用的数据结构是哈希表,线程不安全
保证对象唯一的原理:
先判断hashCode()值,如果都不同就直接加入集合,如果哈希值相同了
在调用equals()方法,如果equals()方法返回值为true,则认为集合中存在该对象,不加入集合
TreeSet:底层使用的数据结构是二叉树,线程不安全
保证集合中对象唯一的方式:依据compareTo()或compare()的返回值是否为0
会对存入集合的对象进行排序
排序方式一:让存入集合中的对象具备可比较性
让存入集合中的对象所属的类实现Comparable<T>接口中的
int compareTo(T t) 方法
排序方式二:让集合具备排序功能
定义一个比较器,实现Comparator<T>接口中的 int compare(T t1,T t2)方法
把比较器对象作为参数传递给TreeSet<E>集合的构造方法
当集合中的对象具备可比较性,且存在比较器时,比较器优先被使用
Map集合:该集合存储键值对,一对一对往里存,而且要保证键的唯一性
|--Hashtable:底层是哈希表数据结构,不可以存入null键null值,该集合是线程同步的。jdk1.0效率低
|--HashMap:底层是哈希数据结构,允许使用null键null值,该集合时不同步的.jdk1.2 效率高
|--TreeMap:底层是二叉树数据结构,线程不同步,可以用于给Map集合中的键进行排序
-
ArrayList与LinkedList区别
ArrayList:使用的数据结构是数组,线程不安全,查找速度快,增删速度慢
LinkedList:使用的数据结构是链表,线程不安全,查找速度慢,增删速度快
-
ConCurrentHashMap实现
-
HashMap源码
- hashmap如何put数据(从hashmap源码角度讲解)?
- 为什么用TreeMap不用HashMap
- HashSet与HashMap怎么判断集合元素重复
HashMap实现
-
SpareArray原理
-
TreeMap具体实现
-
java四种引用
- 强引用置为null,会不会被回收?
Java四种引用
-
sqlite升级,增加字段的语句
-
线程和进程的区别?
进程:是一个正在执行中的程序
每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
线程:就是进程中的一个独立的控制单元
线程在控制着进程的执行
一个进程中至少有一个线程
Java VM 启动的时候会有一个进程java.exe
该进程中至少有一个线程负责java程序的执行
而且这个线程运行的代码存在于main方法中
该线程称之为主线程
扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程
-
为什么要有线程,而不是仅仅用进程?
-
开启线程的三种方式,run()和start()方法区别
- 定义类继承Thread类,复写Thread类中的run方法,调用线程的start()方法
- 实现Runnable接口
- 接口式的匿名内部类new Thread().start()
-
进程调度
-
线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
-
如何保证线程安全?
线程安全问题:多个线程共享数据时,可以出现数据不一致性问题
解决线程安全问题的方式: 加锁
锁(对象锁):Java对象中存一个标志("互斥锁"),保证对象在同一时刻,只能有一个线程去使用(访问)它 ,一个线程访问加锁的对象,其它线程只能等这个线程释放锁后 ,才能访问。
注: 加锁操作后,由于其它线程不停地判断锁是否释放(解锁),所以会影响执行效率
加锁的方式有三种:(synchronized)
1、 同步非静态方法: 在方法声明时,增加synchronized修饰符,针对this对象加锁,如果一个线程访问了这个方法,其它线程在访问this对象的同步方法时,
会进入等待状态,直到这方法执行完成后,才能访问。
2、 同步静态方法:在静态方法声明时,增加synchronized修饰,针对.Class对象加锁, 如果一个线程访问了这个方法,其它线程在访问这个类的同步静态方法时,会进入等待状态,直到这方法执行完成后,才能访问静态函数对应的锁对象是:字节码文件对象 Class 锁的表示: 类名.class
3、 同步代码块:存在同步代码区域,这个区域主要是共享数据的操作(多条语句)
格式: synchronized(对象){ 共享数据的操作语句; } 其中对象是任意的
注:同步函数与同步代码块锁对象不是同一个
Lock接口:
lock()获取锁
unlock()释放锁
ReentrantLock类:接口Lock的实现类
-
死锁
一个线程拿到A资源对象锁后,还想要等着拿到B资源的对象锁,
另一线程拿到B资源对象锁后,还想要等着拿到A资源的对象锁
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 不可剥夺条件: 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁避免与预防
-
并发集合了解哪些
并发集合详解
-
synchronized与Lock的区别
-
CAS介绍
-
volatile用法
-
Java中对象的生命周期
-
进程状态
1.新建状态(New):新创建了一个线程对象。
2.就绪状态(Runnable)(可运行):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4.阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
-
进程保活
-
WebSocket相关以及与socket的区别
-
ReentrantLock 、synchronized和volatile(n面)
多线程断点续传原理
手写生产者/消费者模式
NIO
-
线程如何关闭,以及如何防止线程的内存泄漏
-
volatile synchronize lock 的原理
如何自己实现线程池。线程池内的队列如何管理。线程池大小N的话,连续push进来M个的任务(M>>N),如何处理,比如20大小的线程池扔进来10000个任务
写一个死锁,死锁是怎样产生的,怎样防止死锁
bitmap recycler 相关
算法判断单链表成环与否?
常用数据结构简介
判断环(猜测应该是链表环)
排序,堆排序实现
链表反转
如何保证多线程读写文件的安全?
多线程的方式有哪些?
- new Thread()
- AsyncTask
- Handler
- IntentService
- ThreadPoolExecutor
数据库数据迁移问题
设计模式相关(例如Android中哪里使用了观察者模式,单例模式相关)
x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完
java注解
内部类和静态内部类和匿名内部类,以及项目中的应用
ReentrantLock的内部实现
一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、空间复杂度。手写算法
Http2.0与1.1有啥区别(由这里开始就炸了,完全没复习计算机网络,尤其还是偏背诵的知识的) 5. 有哪些二进制传输协议
差值器&估值器
性能优化如何分析systrace?
string to integer
堆排序过程,时间复杂度,空间复杂度
快速排序的时间复杂度,空间复杂度
手写算法题。一共有3个,面试官随机选择一个。猫扑素数;1到n,求1的个数;单词反转
Http和Https的区别?
1)Https是ssl加密传输,Http是明文传输
2)Https是使用端口443,而Http使用80
3)HttpsSSL+HTTP协议构建的可进行加密传输、身份认证的网络协议要比Http协议安全
4)Https协议需要到CA申请证书
加密算法有哪些?对称加密和非对称加密的区别?
MD5,SHA1,Base64,RSA,AES,DES
对称:使用相同密钥,需要在网络传输,安全性不高。
非对称:使用一对密钥,公钥和私钥,私钥不在网络传输,因此安全性高。
二叉树,给出根节点和目标节点,找出从根节点到目标节点的路径
数据结构中堆的概念,堆排序
排序,快速排序的实现
树:B+树的介绍
二叉树 深度遍历与广度遍历
B树、B+树
图:有向无环图的解释
多叉树的后续遍历
-
网络五层结构
数据结构,搜索二叉树的一些特性,平衡二叉树。
hashmap是如何解决hash冲突的
进程与线程区别
写了一个二分查找和单例模式
http中的同步和异步
java比较重要的几个特性
字符串反转,讨论复杂度
给定一个int型 n,输出1~n的字符串例如 n = 4 输出“1 2 3 4”
输出所有的笛卡尔积组合
单例模式
最长上升子序列
常见编码方式;utf-8编码中的中文占几个字节;int型几个字节
实现一个Json解析器(可以通过正则提高速度)
MVC MVP MVVM; 常见的设计模式;写出观察者模式的代码
TCP的3次握手和四次挥手;TCP与UDP的区别
HTTP协议;HTTP1.0与2.0的区别;HTTP报文结构
HTTP与HTTPS的区别以及如何实现安全性
Java基础
集合类以及集合框架;HashMap与HashTable实现原理,线程安全性,hash冲突及处理算法;ConcurrentHashMap
数据一致性如何保证;Synchronized关键字,类锁,方法锁,重入锁
同步的方法;多进程开发以及多进程应用场景
服务器只提供数据接收接口,在多线程或多进程条件下,如何保证数据的有序到达
ThreadLocal原理,实现及如何保证Local属性
String StringBuilder StringBuffer对比
接口与回调;回调的原理;写一个回调demo;
泛型原理,举例说明;解析与分派
修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法
堆和栈在内存中的区别是什么(数据结构方面以及实际实现方面)
最快的排序算法是哪个?给阿里2万多名员工按年龄排序应该选择哪个算法?堆和树的区别;写出快排代码;链表逆序代码
求1000以内的水仙花数以及40亿以内的水仙花数
子串包含问题(KMP 算法)写代码实现
万亿级别的两个URL文件A和B,如何求出A和B的差集C,(Bit映射->hash分组->多文件读写效率->磁盘寻址以及应用层面对寻址的优化)
蚁群算法与蒙特卡洛算法
写出你所知道的排序算法及时空复杂度,稳定性
百度POI中如何试下查找最近的商家功能(坐标镜像+R树)
前台切换到后台,然后再回到前台,Activity生命周期回调方法。
弹出Dialog,生命值周期回调方法。
多进程场景遇见过么?
关于handler,在任何地方new handler 都是什么线程下
网友评论