DDL (数据定义语言)
数据定义语言 - Data Definition Language
用来定义数据库的对象,比如创建数据表(对数据库和数据表进行操作的)
create drop alter truncate (table)
DML (数据操纵语言)(★★★)
数据处理语言 - Data Manipulation Language
在数据库表中更新,增加和删除记录(对数据表中的表数据进行增删改)
如 update, insert,delete
DCL (数据控制语言)
数据控制语言 – Data Control Language
指用于设置用户权限和控制事务语句(事务)
如grant,revoke,if…else,while,begintransaction
DQL (数据查询语言)(★★★★★)
数据查询语言 – Data Query Language(对数据表中的表数据进行查询)
select
库的操作
创建库:createdatabase 库名 character set 编码表;
删除库:dropdatabase 库名;
查询库:showdatabases;
查看库的编码表:showcreate database 库名;
更改库:use 库名;
查看当前正在使用的库:selectdatabase();
修改库的编码表:alterdatabase 库名 character set 编码表;
表本身的操作
创建表:create table表名( 列名 列的类型(长度) 类的约束 ,列名 列的类型(长度) 类的约束...... );
删除表:drop table 表名;
查询表:show tables;
查看表的结构:desc 表名;
查看表的编码表:showcreate table 表名;
修改表:alter table 表名增/删/改 列名 列的类型(长度) 约束;
add/drop/change/modify
修改表名:renametable 旧表名 to 新表名;
表中数据的操作
增:insert into 表名(列名) values(值);
删:delete from 表名 where 条件; truncate 表名 删除表数据
改:update 表名 set 列名=值 ,列名=值where 条件 ;
查:select 列名 as 别名 ,列名 as 别名… from 表名 where 条件 group by 列名having 条件 order by 排序. -- where 中不能够用聚合函数,也不能‘直接’用别名
查询排重:selectdistinct 列名 from 表名 where 条件;
聚合函数:
count 统计个数、sum求和、avg平均值、max、min
在使用这几个函数进行数据的统计分析时,有时需要对数据表中的列进行数据的分组处理。group by
分组 group by :
排序:order by 列名 asc | desc;
约束:
主键约束:primary key id int primarykey auto_increment
唯一约束:unique 内容不允许重复,可以为null(null不算重复)。
非空约束:not null 。不允许为空。 表示该列的内容不允许为空。
删除:清空数据
truncate table 表名
通过删除整张表之后再重新创建一张表来达到清空数据的目的。
delete 和 truncate的区别是delete删除的数据在事务管理的情况下还能恢复,而truncate则不能恢复。
CRUD:create read / retrive update delete
语法:mysqldump -u 用户名 -p 数据库名 > 磁盘SQL文件路径
恢复方式:
1种: 创建新库 导入:source 路径
2种: 创建新库 语法:mysql -u 用户名 -p 导入库名< 硬盘SQL文件绝对路径
注意:在cmd下使用,不是登录mysql后使用,和备份比较类似,只不过mysql后面不带dump,并且箭头是<,指向需要导入数据的新的数据库。
一对多
建表之后添加外键约束
一,主表
多,从表
语法 : alter table 从表名称 add foreign key (外键列的名称) references 主表名称(主键)
alter table employee add dept_id int;
alter table employee add foreign key (dept_id) references dept(id);
建表时添加外键约束;
createtable employee(
id int primary key auto_increment,
name varchar(20),
age int ,
salary double,
dept_id int,
foreign key(dept_id) references dept(id)
);
自关联
创建表的语句
create tablearea(
id varchar(10)primary key,
name varchar(10),
descriptionvarchar(10),
parent_idvarchar(10),
foreignkey(parent_id) references area(id)
);
多对多
关联主键
需要中间表来存放双方的键
-- 创建中间表
createtable coder_project(
coder_id int,
project_id int,
foreignkey (coder_id) references coder(id),
foreignkey (project_id) references project(id)
);
一对一
关联主键
任意一方设置一个外键关联即可
内连接:
1、 隐式内连接:
Select * from a,b where a.id = b.id;
结果:C
2、 显示内连接:
Select * from a inner join b on a.id = b.id;
结果:C
外连接:
1、 左外连接
select * from a left outer join b on a.id = b.id
结果:A+C
2、 右外连接
select * from a right outer join b on a.id = b.id
结果:B+C
3、 union:相当于全外连接
select * from a left outer join b on a.id = b.id
union
select * from a right outer join b on a.id = b.id
结果:A+B+C,会自动虑重
select * from a left outer join b on a.id = b.id
union all
select * from a right outer join b on a.id = b.id
结果:A+B+C,有重复数据
in (20,30):表示条件是20或者30,类似于添加条件select* from student where age = 20 or age = 30;
-- limit 限制查询结果返回的数量
select * from student limit 5;
-- select * from student limitoffset,count;
select * from student limit 5,5;
-- 分页查询
select * from student limit 0,3; -- 1
select * from student limit 3,3; -- 2
select * from student limit 6,3; -- 3
注意 : 不可以使用 id = …; 语句, 因为 id =之后只能存放一个值, in 可以表示多个值取其中一个.
l all:表示所有,和union一起使用。
左连接和右连接查询结果的合集。
union all :不去掉重复进行合并,相当于查询一次左外连接,然后再查询一次右外连接,然后将两次的查询结果合并。
l any : 表示任何一个
查询部分数据
a> any(1,3,5,6) 相当于 a>1 相当于 a > min(1,3,5,6);
a=any(1,3,5,6) 相当于 a in(1,3,5,6) 或者 a=1 or a=3 or a=5 or a=6
注意:any的后面需要跟语句,不能直接放值。
case when … then…when…then…else…end
select ename,sal,case when sal >= 3000then'3级' when sal >=2000 then '2级' else '1级' end as 级别 from emp;
基本查询代码
public static void main(String[] args) throws Exception {
//注册驱动
Class.forName("com.mysql.jdbc.Driver");
//建立连接
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/day03", "root", "123");
//创建statement对象发送sql语句
Statement sta = con.createStatement();
String sql = "select * from emp";
//向数据库发送并返回结果
ResultSet rs = sta.executeQuery(sql);//查询方法//sta.executeUpdate(sql)增删改方法返回修改成功行数
//遍历集合
while(rs.next()){
String name = rs.getString("ename");
String id = rs.getString("empno");
System.out.println(id+"---"+name);
}
//关流,后开先关
rs.close();
sta.close();
con.close();
}
Statement.execute(sql) //返回boolean类型,true查询返回结果集,false增删改.
重点掌握:
executeQuery(sql) 执行select语句。
executeUpdate(sql) 执行insert update delete 语句。返回 修改成功行数
结果集 getXXX(列名/索引第几列) XXXJava中数据类型
释放资源(关流)
Jdbc中,连接的资源是非常宝贵的。所以我们用完了,务必要保证资源被释放掉。哪怕程序出现异常,也需要释放资源。对于这样一个情况,我们需要将资源释放的操作放在finally代码块中。
释放资源应该放在finally代码块中
finally{
//释放资源
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(statement!=null){
try {
statement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(con!=null){
try {
con.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
JDBC编程步骤
1注册驱动
Class.forname(“com.mysql.jdbc.Driver”);
2获取连接
Connection con =DriverManager.getConnection(url,user,password);
3.创建statement对象
Statement st = con.createStatement();
4.发送sql并且执行
ResultSet rs = st.executeQuery();
5.遍历结果集/返回增删改成功行数
While(rs.next()){
}
6.释放资源 放在finally中
抽取工具类
public class JdbcUtils {
public static String driverClass = "";
public static String url = "";
public static String user = "";
public static String pwd = "";
//静态代码块类加载的时候加载,获取配置文件里的数据库信息
static {
try {
Properties p = new Properties();
p.load(new FileInputStream("jdbc.properties"));
driverClass = p.getProperty("driverClass");
url = p.getProperty("url");
user = p.getProperty("user");
pwd = p.getProperty("pwd");
Class.forName(driverClass);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() {
//与数据库获取连接传入加载的配置文件内数据库信息
Connection con = null;
try {
con = DriverManager.getConnection(url, user, pwd);
} catch (SQLException e) {
e.printStackTrace();
}
return con;
}
//关闭资源(关流)
public static void release(Connection con, Statement sta, ResultSet res) {
if (res != null) {
try {
res.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (sta != null) {
try {
sta.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
sql注入问题
Connection的PreperedStatement方法
setString方法
相对于Statement对象而言:
1、PreperedStatement可以避免SQL注入的问题。
2、Statement对象每次执行SQL语句时,都会对其进行编译。当相同的SQL语句被执行被执行多次时,Statement对象就会使数据库频繁编译相同的SQL语句,从而降低数据库的效率。
PreparedStatement对象可对SQL语句进行预编译。也就是说,当相同的SQL语句再次执行时,数据库只需使用缓冲区中的数据,而不需要对SQL语句在次编译,从而提高访问效率。
3、并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写。可读性变强。
测试类
public static void main(String[] args) throws Exception {
//前端获取用户名密码
String user = "qwe";
String password = "asd";
//sql查询语句,比对位置用sql通配符?代替来增加安全性防止sql语句被破坏
String sql = "select count(*) from userswhere user = ? and password = ?";
//调用方法获取与数据库的连接
Connection con = JdbcUtils.getConnection();
//创建PreparedStatement对象,进行预编译
PreparedStatement sta = con.prepareStatement(sql);
//第一个参数表示第几个问号,第二个参数表示传入的数据,写入编译好的sql语句中
sta.setString(1, user);
sta.setString(2, password);
ResultSet rs = sta.executeQuery();
while(rs.next()){
//如果数据库内存在数据那么返回统计到的1个
int x = rs.getInt("count(*)");
if(x == 1){
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
}
//调用自定义方法关闭资源(关流)
JdbcUtils.release(con, sta, rs);
}
用来存放多个连接.从而不需要每次都创建连接.每次创建连接效率低并发多的时候容易造成宕机.
自定义连接池
自定义数据库连接池需要实现 java.sql.DataSource 这个接口.
使用linkedList集合存放连接,查询慢增删快,连接需要移出使用再添加进去(不能关闭资源动态代理close方法).
定义一个静态linkedList集合来存放连接
构造方法中 循环向集合中添加连接..first添加
getConnection方法中 last取出连接 并使用动态代理重写closs方法 addfirst接口 下面返回null. 如果不是close则返回原来的状态
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String name = method.getName();
if("close".equals(name)){
pools.addFirst(con);
return null;
}
return method.invoke(con, args);
}
getConnection方法返回Connection连接
getConnection方法中last取出连接并return;
DBCP开源连接池
手动配置数据库信息
ds.setDriverClassName
ds.setUrl
ds.setUserName
ds.setPassword
Properties获取配置文件
然后获取数据源
BasicDataSourceFactory.createDataSource(properties);
整体参考整体代码:
public static void main(String[] args) throws Exception {
//首先获取Properties配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("src/com/hxc/datasource/jdbc.properties"));
//然后创建dbcp数据库连接池
DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);
//获取连接
Connection connection = dataSource.getConnection();
//创建preparedstatement对象,执行sql语句
PreparedStatementprepareStatement = connection.prepareStatement("insert into uservalues(?,?,?,?)");
prepareStatement.setString(1, "122");
prepareStatement.setString(2, "u2u");
prepareStatement.setInt(3, 22);
prepareStatement.setString(4, "123");
int executeUpdate = prepareStatement.executeUpdate();
//释放资源
prepareStatement.close();
connection.close();
}
C3P0开源连接池
注意:1.c3p0的xml配置文件的名称必须叫做c3p0-config.xml
2. C3p0-config.xml必须放在src目录下。
//首先创建c3p0的数据库连接池 ,c3p0数据库连接池直接去寻找配置文件,自动解析
ComboPooledDataSourcedataSource = new ComboPooledDataSource();
//获取连接
Connection connection = dataSource.getConnection();
//创建preparedstatement对象,执行sql语句
连接池总结
经典三层结构(Web\service\DAO)
web层
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = sc.nextLine();
System.out.println("请输入密码:");
String password = sc.nextLine();
//封装对象验证用户名和密码
User user = new User(0,username,password,null);
//创建service层对象
ServiceDemo serviceDemo = new ServiceDemo();
//向service层传递需要验证的User
//并返回一个验证后的存储读取到的用户信息User
User daoUser = serviceDemo.findNamePwd(user);
//判断返回的是否为空
if(daoUser!=null){
System.out.println("登录成功"+daoUser);
}else{
System.out.println("登录失败");
}
}
service层
public User findNamePwd(User user) {
//创建dao层对象
DaoDemo daoDemo = new DaoDemo();
//用对象调用方法传递和返回参数
User daoUser =null;
try {
daoUser = daoDemo.findNamePwd(user);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//返回给web层验证结果
return daoUser;
}
dao层
public User findNamePwd(User user) throws Exception {
DataSource ds = JdbcUtils.getDataSource();
Connection con = ds.getConnection();
String sql = "select * from user whereusername = ? and password = ?";
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
User daoUser = null;
while(rs.next()){
int id = rs.getInt("id");
String username = rs.getString("username");
String password = rs.getString("password");
String email = rs.getString("email");
//把读取到的数据封装传回
daoUser = new User(id,username,password,email);
}
rs.close();
ps.close();
con.close();
//把数据传回调用方法的service层
return daoUser;
}
DBUtils学习
1、QueryRunner 框架核心类 ,所有数据库操作都是必须通过 QueryRunner 进行的
2、ResultSetHandler 结果集封装接口,完成将ResultSet 结果集 封装为一个Java对象
3、DbUtils 工具类提供驱动管理、事务管理、释放资源等一系列公共方法
构造器:
一\需要手动管理事务
QueryRunner() -------- 没有传递连接池给DBUtils 框架,框架不能获得数据库连接,接下来每个操作,必须将数据库连接传给框架 (手动管理事务)
二\由框架管理事务
QueryRunner(DataSource ds) ---- 将连接池给DBUtils框架,以后每个操作,都会从连接池中获取一个新的连接 (每条SQL 一个单独的事务)
更新操作 insert updatedelete
public int update(Connection conn, String sql,Object... params) ---- 手动管理事务,没有将连接池提供框架,传入连接
public int update(String sql, Object... params)----- 将连接池交给框架,由框架管理事务,不需要传入连接
查询操作 select
public Object query(Connection conn, String sql,ResultSetHandler
public Object query(String sql,ResultSetHandler
增删改
public void test_insert() throwsSQLException {
//1. 创建QueryRunner 对象, 同时将 `数据库` 对象传入
QueryRunner queryRunner =new QueryRunner(JDBCUtils.getDataSource());
//2. 执行 update 方法
String sql = "insertinto user values(?,?,?,?);";
Object[] params = {5, "小七", "123", "[email protected]"};
queryRunner.update(sql, params);
}
查
public void test_query() throwsSQLException {
//1. 创建QueryRunner 对象, 同时将 `数据库` 对象传入
QueryRunner queryRunner =new QueryRunner(JDBCUtils.getDataSource());
//2. 执行 query 方法
String sql = "select* from user where id = ?;";
User user = queryRunner.query(sql, new ResultSetHandler
@Override
publicUser handle(ResultSet rs) throws SQLException {
这里对返回集进行操作
return user;
}
return null;
}
}, 3);
System.out.println(user);
}
ResulSetHandler的实现类使用
对结果集进行封装处理
常用3种
BeanHandler
@Test
public void test_BeanHandler() throwsSQLException {
//1. 创建QueryRunner对象, 同时将 `数据库` 对象传入
QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDataSource());
//2. 执行 query 方法
String sql = "select* from user where id=?;";
User user = queryRunner.query(sql, new BeanHandler
System.out.println(user);
}
BeanListHandler
@Test
public void test_BeanListHandler() throws SQLException {
//1. 创建QueryRunner对象, 同时将 `数据库` 对象传入
QueryRunner queryRunner =new QueryRunner(JDBCUtils.getDataSource());
//2. 执行 query 方法
String sql = "select* from user;";
List
for (User user : list){
System.out.println(user);
}
}
ScalarHandler
@Test
public void test_scalarHandler() throwsSQLException {
//1. 创建QueryRunner对象, 同时将 `数据库` 对象传入
QueryRunner queryRunner =new QueryRunner(JDBCUtils.getDataSource());
//2. 执行 query 方法
String sql = "selectcount(*) from user;";
Long result = queryRunner.query(sql, new ScalarHandler
System.out.println(result);
}
事务
1、原子性(Atomicity)
事务包装的一组sql,要么都执行成功,要么都失败。这些操作是不可分割的。
2、一致性(Consistency)
数据库的数据状态是一致的。
事务的成功与失败,最终数据库的数据都是符合实际生活的业务逻辑。一致性绝大多数依赖业务逻辑和原子性。
3、持久性:(Durability)
事务成功提交之后,对于数据库的改变是永久的。哪怕数据库发生异常,重启之后数据亦然存在。
4、隔离性(Isolation)
一个事务的成功或者失败对于其他的事务是没有影响。2个事务应该相互独立。
事务的并发问题
1.脏读:指一个事务读取了另外一个事务 未提交的数据
2.不可重复读:在一个事务内多次读取表中的数据,多次读取的结果不同(内容修改,已提交)
3.幻读(虚读):在一个事务内多次读取数据的数量,多次读取的结果不同(数量增删)
隔离级别
1.安全性:serializable > repeatable read > readcommitted > read uncommitted
2.性能 : serializable < repeatable read < readcommitted < read uncommitted
简单总结:
开启事务 |
|
执行sql语句群 |
|
出现异常 回滚事务(撤销) |
无异常 事务提交(生效) |
mysql事务操作
sql语句 |
描述 |
start transaction; |
开启事务 |
commit; |
提交事务 |
rollback; |
回滚事务 |
|
|
JDBC事务操作
Connection对象的方法名 |
描述 |
conn.setAutoCommit(false) |
开启事务 |
conn.commit() |
提交事务 |
conn.rollback() |
回滚事务 |
优化工具类
//避免事务当中的连接与业务当中的连接可能发生的问题,ThreadLocal保证整个业务都使用同一个连接
private static ThreadLocal
public static Connection getConnection(){
//从Map集合中先获取
Connection con = conTl.get();
//如果为空那么获取连接池中的一个连接存入
if(con == null){
try {
con = cpds.getConnection();
conTl.set(con);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//返回这个连接
return con;
}
//优化:自定义保证使用同一个工具类
public static void commitAndClose(Connection con){
//使用DbUtils当中的提交事务和关闭资源
DbUtils.commitAndCloseQuietly(con);
//使用完连接从集合中移出
conTl.remove();
}
public static void rollbackAndClose(Connection con){
//使用DbUtils当中的回滚事务和关闭资源
DbUtils.rollbackAndCloseQuietly(con);
//使用完连接从集合中移出
conTl.remove();
}
service层
DaoDemo dao = new DaoDemo();
//从map集合获得连接
Connection con = JdbcUtils.getConnection();
//定义返回结果
boolean result = false;
try {
//关闭自动开启事务
con.setAutoCommit(false);
dao.outUser(outUser,money);
dao.inUser(inUser,money);
//顺利执行提交事务 并把集合中连接移回连接池
result = true;
JdbcUtils.commitAndClose(con);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
//出现异常则回滚事务
JdbcUtils.rollbackAndClose(con);
}
return result;
dao层
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
Connection con = JdbcUtils.getConnection();
String sql = "update account set money =money - ? where name = ?";
//最后参数为可变参数(实际源码采用接口回调)
qr.update(con, sql, money,outUser);
##1. 事务的ACID
1.四大特性acid
2.事务的并发问题
1.脏读:指一个事务读取了另外一个事务 未提交的数据
2.不可重复读:在一个事务内多次读取表中的数据,多次读取的结果不同(内容修改,已提交)
3.幻读(虚读):在一个事务内多次读取数据的数量,多次读取的结果不同(数量增删)
3.隔离级别
1.安全性:serializable > repeatable read > readcommitted > read uncommitted
2.性能 : serializable < repeatable read < readcommitted < read uncommitted
##2. 综合案例
##3. 总结
1. Mysql:前三天学习的主要是是sql语法和查表逻辑,详解笔记
2. JDBC总结
1.JDBC六步(工具类(JdbcUtils和DButils)都是对这六步的优化)
2.经典三层开发.(web的引入,我们现在主要学习dao层)
3. 知识点逻辑
1. 连接池
1. 引入:由于java与数据库反复断开,创建会造成资源浪费(宕机),引入连接池,首先我们了解自定义连接池的原理:实现datasource接口(jdbc2.0规范)
2. 自定义连接池
1. 概念连接池中核心的概念便是将多个连接存入一个集合中去,用的时候去集合中取,用完放回集合.那么其核心便是对close方法进行重写.
2. 对于close方法的重写我们引入两种方式
1.自定义close方法(需重命名方法名,不利于记忆)
2.对Connection接口中的close方法进行复写,而复写close方法我们又引入了两种方式:装饰设计模式和动态代理.
1.装饰设计模式有一弊端,就是必须复写Connection用到的所有方法
2.而动态代理却完美的避免,用代理类去拦截colse方法,让代理类去完成。连接放回集合的操作,其他的方法全部放行.
3. 连接池框架:了解连接池原理之后,我们引入了DBCP及C3P0
1. DBCP使用:需要先将外部的Properties文件读取到内存,然后底层开始对配置文件开始读取,完成连接池的创建及初始化
2. C3P0使用:自动对SRC目录下的c3p0-config.xml文件的读取,完成连接池的创建及初始化.
3. 注意:两者需要共同注意的是,配置文件和xml文件中对于键(标签)名必须按照其规范,不能改动.
2. 框架DButils
1. 引入:由于Dao层对于数据的处理过于繁琐,其本质在于JDBC中 创建执行语句, 执行语句, 处理数据这三步代码繁琐.
2. 核心API: QueryRunner类,该类的两个核心方法:query update 方法
3. 自定义QueryRunner
1.引入:为了更好的理解该类,我们对QueryRunner类进行了自定义 ,了解了query update方法底层的原理.
2.update 中核心的代码便是 对于sql预编译语句中占位数的计算(元数据)
3.而query除包含该核心代码外,还有对结果集的处理,自此引入了ResultSetHandler接口(结果回调)
4.常用的ResultSetHandler三个实现类
1.BeanHandler(返回一个bean对象)
2.BeanListHandler(返回一个存储bean对象集合)
3.ScalerHandler(返回一个计数值(count(*)))
3. 事务
1. 引入:由转账业务的异常引入事务的概念(一个成功,一个失败,不能容忍)
2. 事务的核心便是事务执行,成功提交,失败回滚.
1.sql的语法
2.代码的实现
1.核心:保证Service层和Dao层的连接一致
2.在单一线程的情况下:我们可以采用两种方式去处理
1.单例设计模式(缺陷:多线程并发问题)
2.Connection作为参数传递(service层和dao层代码耦合)
3. ThreadLocal类:
1.多线程的情况下,我,为了更好的理解该类,我们自定义了一个Map集合,该集合键存储当前线程名,值对应该线程下获得的连接.
2.其本质还是单例,不过是进行线程绑定的单例(key是当前线程)
##3. :工厂设计模式
1. 工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。
# 4. 网站(程序员之家)
1. 掘金网(技术分享)
2. 开发者头条(技术分享)
2. CSDN(技术博客)
4. Github(全球最大的开源网站)
5. Stack Overflow(问答网站,全英文)
6. 果壳网(科技类)
7. 知乎(中文知识问答社区,程序员比较多)