- 投稿日:2021-12-24T23:41:09+09:00
Androidネイティブの実装をFlutterでリプレースしたらコード量がすごく減った話
はじめに この記事を書くことになった経緯としては、最初にJavaで実装を進めていました。 ですが、開発途中で宣言的UIのフレームワークの良さや凄さを知る機会があり、それによりFlutterを勉強してFlutteで実装したいと思い始めました。 そこで、今作っているものをFlutterで書いたらどうなるのかと思い立ちFlutterでの実装も進めました。 結果として、同じ機能をFlutterで実装してみたところ驚くほど少ないコード量で実装できたので、せっかくだと思い記事にまとめることにしました。 紹介する実装の部分 「Firestoreから取得したデータをGridViewで表示する実装」 の部分について紹介していきたいと思います。 Androidネイティブの方の実装ではMVVMモデルでクラス分けをした実装になっていますが、Flutterの方はまだ勉強して日が浅いためアーキテクチャモデルまで意識した作りにはなっていなかったりします。 なので実装をするアーキテクチャの前提は違うかもしれませんが、同じ機能を実現しようとした時にどんな感じで実装が出来るのかという視点で見てもらえれば良いかと思います。 完成画面イメージ Firestoreにお酒の名前の一覧のデータを置いておき、そのデータを取得してグリッドで表示するというごく簡単なものです。 左がAndroidネイティブで右がFlutterです。 (獺祭あり過ぎですね・・・) Androidネイティブでの実装 Androidネイティブでの実装ではJavaで書いたコードで紹介していきたいと思います。1点目は画面レイアウトの定義、2点目はそのレイアウトを利用した実装についてです。 画面レイアウトの定義 まず、1つ1つのアイテムをグリッド形式で表示するためのレイアウトを定義します。 CardViewのウィジェットを利用することで簡単に角丸のグリッドアイテムが作成できます。 grid_myitem.xml <?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" app:cardCornerRadius="6dp" app:cardElevation="6dp" app:contentPadding="8dp" app:cardUseCompatPadding="true"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" tools:ignore="UseCompoundDrawables"> <ImageView android:id="@+id/myitem_image_view" android:layout_width="match_parent" android:layout_height="160dp" android:contentDescription="@string/myitem_image" /> <TextView android:id="@+id/myitem_text_view" android:textSize="20sp" android:textColor="@android:color/black" android:textAlignment="center" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout> </androidx.cardview.widget.CardView> 次に、上記で定義したアイテムをグリッド形式で表示するために、FragmentにRecyclerViewを定義します。 fragment_home.xml ・・・ <androidx.recyclerview.widget.RecyclerView android:id="@+id/myitem_recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content" tools:ignore="MissingConstraints" /> ・・・ Firestoreから取得したデータを表示する実装 上記の画面レイアウトを利用して画面に表示するまでの実装を以下に書いていきます。 まず、CardViewをRecyclerViewにinflateする処理を実装します。 HomeFragnemt.java public class HomeFragment extends Fragment { private HomeViewModel homeViewModel; private FragmentHomeBinding binding; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class); binding = FragmentHomeBinding.inflate(inflater, container, false); View root = binding.getRoot(); final RecyclerView recyclerView = binding.myitemRecyclerView; HomeListAdapter homeListAdapter = new HomeListAdapter(new HomeListAdapter.HomeListDiff(), this::onAdapterClicked); recyclerView.setAdapter(homeListAdapter); recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3, RecyclerView.VERTICAL, false)); homeViewModel.getBrandIdentifierList().observe(getViewLifecycleOwner(), brandIdentifiers -> homeListAdapter.submitList(brandIdentifiers)); } } 次に、Firestoreから取得したデータをMVVMモデルでRecyclerViewにバインドするまでの実装を書きます。 まずは、RecyclerViewに設定するアダプターの定義をして・・・。 HomeListAdapter.java public class HomeListAdapter extends ListAdapter<BrandIdentifier, HomeViewHolder> { protected HomeListAdapter(@NonNull @NotNull DiffUtil.ItemCallback<BrandIdentifier> diffCallback) { super(diffCallback); } @NonNull @Override public HomeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.grid_myitem, parent, false); return new HomeViewHolder(view); } @Override public void onBindViewHolder(@NonNull HomeViewHolder holder, int position) { String id = getItem(position).getId(); String name = getItem(position).getTitle(); holder.bind(id, name); } public static class HomeListDiff extends DiffUtil.ItemCallback<BrandIdentifier> { @Override public boolean areItemsTheSame(@NonNull @NotNull BrandIdentifier oldItem, @NonNull @NotNull BrandIdentifier newItem) { return false; } @Override public boolean areContentsTheSame(@NonNull @NotNull BrandIdentifier oldItem, @NonNull @NotNull BrandIdentifier newItem) { return false; } } } 次に、アダプターに設定するデータホルダーを定義します。 HomeViewHolder.java public class HomeViewHolder extends RecyclerView.ViewHolder { private String mId; private ImageView mMyItemImage; private TextView mMyItemText; public HomeViewHolder(@NonNull @NotNull View itemView) { super(itemView); mMyItemImage = (ImageView) itemView.findViewById(R.id.myitem_image_view); mMyItemText = (TextView) itemView.findViewById(R.id.myitem_text_view); } public void bind(String id, String name) { mId = id; mMyItemImage.setImageResource(R.drawable.ic_ponshu); mMyItemText.setText(name); } } そしてバインドするためのViewModelを実装して・・・。 HomeViewModel.java public class HomeViewModel extends AndroidViewModel { private PonshuRepository mRepository; public HomeViewModel(@NonNull Application application) { super(application); mRepository = PonshuRepository.getInstance(); } public LiveData<List<BrandIdentifier>> getBrandIdentifierList() { return mRepository.getBrandIdentifierList(); } } 最後に、Firestoreからデータを取得するリポジトリを実装します。 PonshuRepository.java public class PonshuRepository { private static PonshuRepository mInstance; private FirebaseFirestore mDb; private CollectionReference mCollectionRef; private MutableLiveData<List<BrandIdentifier>> mBrandNameList; public static PonshuRepository getInstance() { if (mInstance == null) { mInstance = new PonshuRepository(); } return mInstance; } private PonshuRepository() { mDb = FirebaseFirestore.getInstance(); mCollectionRef = mDb.collection("BrandList"); } public LiveData<List<BrandIdentifier>> getBrandIdentifierList() { if (mBrandNameList == null) { mBrandNameList = new MutableLiveData<>(); } mCollectionRef.get() .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() { @Override public void onComplete(@NonNull Task<QuerySnapshot> task) { if (task.isSuccessful()) { List<BrandIdentifier> brandIdentifierList = new ArrayList<>(); for (QueryDocumentSnapshot document : task.getResult()) { Map<String, Object> brandData = (Map<String, Object>) document.getData(); brandIdentifierList.add(new BrandIdentifier(document.getId(), (String) brandData.get("title"))); } mBrandNameList.setValue(brandIdentifierList); } else { Log.d("FIREBASE", task.getException().toString()); } } }); return mBrandNameList; } } Flutterでの実装 Flutterでの実装は、これから紹介する1ファイルだけの実装になります。 Flutterは宣言的UIのフレームワークであるため、上記で紹介したようなレイアウト用のXMLファイルと機能の実装ファイルを分ける事なく、レイアウト+実装がまとめて書けます。なので、 レイアウト部分についてはGridView.builderを利用してグリッドを生成 Firestoreからのデータ取得をStreamBuilderで取得 となります。実装は以下の通りです。 home_view.dart class SakeHomeViewWidget extends StatelessWidget { final Color color; final String title; const SakeHomeViewWidget({Key? key, required this.color, required this.title}) : super(key: key); @override Widget build(BuildContext context) { var assetsImage = "images/ic_sake.png"; return Scaffold( body: StreamBuilder( stream: FirebaseFirestore.instance.collection('Brands').snapshots(), builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) { if (!snapshot.hasData) { return const Center( child: CircularProgressIndicator(), ); } return GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 10.0, // 縦 mainAxisSpacing: 10.0, // 横 childAspectRatio: 0.7), itemCount: snapshot.data!.docs.length, padding: const EdgeInsets.all(5.0), itemBuilder: (BuildContext context, int index) { return Container( child: GestureDetector( onTap: () {}, child: Column( children: <Widget>[ Image.asset(assetsImage, fit: BoxFit.cover,), Container( margin: const EdgeInsets.fromLTRB(8, 8, 8, 0), child: Text( snapshot.data!.docs[index]['title'], maxLines: 2, overflow: TextOverflow.ellipsis,), ), ], )), padding: const EdgeInsets.all(10.0), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(10), boxShadow: const [ BoxShadow( color: Colors.grey, offset: Offset(5.0, 5.0), blurRadius: 10.0, ) ], ), ); }, ); }), ); } } 基本的にはこれだけで実装ができてしまいます。非常に短いですね。 同じ機能をこれだけ短いコード量で実現出来るのであれば、Flutter勉強しようかなって気になりませんか? おわりに いかがでしたでしょうか。Androidネイティブ(特にJava)で書いていたものをFlutterにするとこれだけコード量が少なく、同じ機能が実現できました。 Androidネイティブの開発でもJavaで書くのではなくKotlinを使う、Jetpack Composeを使う等をすればもっとコード量を少なく書くことはできます。 この記事で言いたかったのは、 「宣言的UIのフレームワークはモバイルアプリ開発を行う上で非常にメリットのあるものなんだ」 と言うことでした。 なので今後もFlutterの勉強を進めて、様々なライブラリを使いこなせるようになったりアーキテクチャを採用した設計にしていきたいと思います。
- 投稿日:2021-12-24T23:34:08+09:00
Java レイジー エバリュエーション
Javaのカレンダー | Advent Calendar 2021の16日目の記事です I will always choose a lazy person to do a difficult job. Because he will find an easy way to do it - Bill Gates ※ 私はいつも難しい仕事は怠け者にやってもらいます。なぜなら、怠け者は楽なやり方を見つけてくれるからです。 - ビル・ゲイツ 概要 Java の遅延評価(lazy evaluation)を Stream API 経由で実行した場合の性能を見てみます Java ストリーム で検証したように、1000件以上のオブジェクトを処理してみます 結果は、大雑把に表現するとヒープも処理速度も10倍以上、 Stream API を利用したほうが良いと思いました(感想) Stream API の fliter (Predicate) は遅延評価ではないといわれてしまうと... 比較したコード コードは適当なオブジェクトが1000個(、1万個、10万個)入ったリストからある条件(今回は年齢が30以上)を抽出するものです Stream API の場合 void useStream() { l.stream().filter(p -> p.age >= 30).collect(Collectors.toList()); } Stream API で parallel をつけた場合 void useStreamWithParallel() { l.stream().filter(p -> p.age >= 30).parallel().collect(Collectors.toList()); } List を使う場合 void useList() { List<Parson> res = new ArrayList<>(); for (var p : l) { if (p.age < 30) { continue; } res.add(create()); } } 測定結果 測定には、以前の記事 Javaでヒープサイズ測定 JUnit5編 で使用した、quickperf と Javaでベンチマーク(性能測定) JUnit5編 で紹介した jmh を使いました Stream API の parallel を推したいのですが、なかなか結果がついてこないので、今回は欲張って1万件、10万件も検証しました。 スコアは、parallelなしの Stream API に比べ、parallelありの場合が、1万件でやや劣勢、10万件で逆転しました JVM のヒープサイズ 1000 件 [QUICK PERF] Measured heap allocation (test method thread): 21.44 Kilo bytes (21 952 bytes) ← Stream API [QUICK PERF] Measured heap allocation (test method thread): 50.95 Kilo bytes (52 176 bytes) ← Stream API (parallel) [QUICK PERF] Measured heap allocation (test method thread): 264.87 Kilo bytes (271 224 bytes) ← List 10000 件 [QUICK PERF] Measured heap allocation (test method thread): 121.70 Kilo bytes (124 616 bytes) ← Stream API [QUICK PERF] Measured heap allocation (test method thread): 160.49 Kilo bytes (164 344 bytes) ← Stream API (parallel) [QUICK PERF] Measured heap allocation (test method thread): 2.45 Mega bytes (2 573 952 bytes) ← List (単位が違う) 10000 件 [QUICK PERF] Measured heap allocation (test method thread): 845.55 Kilo bytes (865 848 bytes) ← Stream API [QUICK PERF] Measured heap allocation (test method thread): 823.67 Kilo bytes (843 440 bytes) ← Stream API (parallel) [QUICK PERF] Measured heap allocation (test method thread): 24.67 Mega bytes (25 870 512 bytes) ← List (単位が違う) ベンチマーク 1000 件 Benchmark Mode Cnt Score Error Units StreamVsListLazyPerfTest.useList thrpt 5 1171.093 ± 22.063 ops/s StreamVsListLazyPerfTest.useStream thrpt 5 218394.756 ± 4049.119 ops/s StreamVsListLazyPerfTest.useStreamWithParallel thrpt 5 68465.691 ± 1838.747 ops/s 10000 件 Benchmark Mode Cnt Score Error Units StreamVsListLazyPerfTest.useList thrpt 5 118.407 ± 3.344 ops/s StreamVsListLazyPerfTest.useStream thrpt 5 21036.060 ± 379.085 ops/s StreamVsListLazyPerfTest.useStreamWithParallel thrpt 5 18515.519 ± 3244.870 ops/s 100000 件 Benchmark Mode Cnt Score Error Units StreamVsListLazyPerfTest.useList thrpt 5 10.984 ± 0.102 ops/s StreamVsListLazyPerfTest.useStream thrpt 5 813.617 ± 86.894 ops/s StreamVsListLazyPerfTest.useStreamWithParallel thrpt 5 2140.738 ± 79.030 ops/s 付録 ヒープ測定用のテストコード @QuickPerfTest public class StreamVsListLazyTest { List<Parson> l = new ArrayList<>(); @BeforeEach void beforeAll() { for (int i = 0; i < 10000; i++) { l.add(create()); } } @MeasureHeapAllocation @Test void useStream() { l.stream().filter(p -> p.age >= 30).collect(Collectors.toList()); } @MeasureHeapAllocation @Test void useStreamWithParallel() { l.stream().filter(p -> p.age >= 30).parallel().collect(Collectors.toList()); } @MeasureHeapAllocation @Test void useList() { List<Parson> res = new ArrayList<>(); for (var p : l) { if (p.age < 30) { continue; } res.add(create()); } } @AllArgsConstructor @Data class Parson { private String name; private int age; private String addr; private String addr2; } Parson create() { return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(100), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20)); } } ベンチマーク取得用のテストコード @State(value = Scope.Benchmark) public class StreamVsListLazyPerfTest { List<Parson> l = new ArrayList<>(); @Setup public void setup() { for (int i = 0; i < 10000; i++) { l.add(create()); } } @Benchmark public void useStream() { l.stream().filter(p -> p.age >= 30).collect(Collectors.toList()); } @Benchmark public void useStreamWithParallel() { l.stream().filter(p -> p.age >= 30).parallel().collect(Collectors.toList()); } @Benchmark public void useList() { List<Parson> res = new ArrayList<>(); for (var p : l) { if (p.age < 30) { continue; } res.add(create()); } } // Junit のテストアノテーションで Runner を設定する @Test void benchMark() throws RunnerException { Options opt = new OptionsBuilder() .include(StreamVsListLazyPerfTest.class.getSimpleName()) .forks(1) // 1回実行 .warmupIterations(1) // 1回繰り返し .build(); new Runner(opt).run(); } @AllArgsConstructor @Data class Parson { private String name; private int age; private String addr; private String addr2; } Parson create() { return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(100), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20)); } }
- 投稿日:2021-12-24T21:28:56+09:00
【Java】いろいろな方法でMapをループさせてみる
まえがき Mapのループを忘れがちなので書き残しておきます。 こんな感じのMapをループさせてみます。 Map<String, String> map = new HashMap<>(); map.put("apple", "りんご"); map.put("strawberry", "いちご"); map.put("grape", "ぶどう"); 拡張for文を使ったループ for (String key : map.keySet()) { System.out.println(key + " は " + map.get(key) + " です"); } apple は りんご です strawberry は いちご です grape は ぶどう です ラムダ式を使ったループ map.forEach((key, value) -> System.out.println(key + " は " + value + " です")); apple は りんご です strawberry は いちご です grape は ぶどう です 元記事 詳細は元記事へ!
- 投稿日:2021-12-24T20:13:08+09:00
SpringBoot × MyBatis 中間テーブルへ登録する方法
はじめに 今回は、自作している学習用チャットアプリで中間テーブルを実装したところ、かなり詰まったので、中間テーブルへの登録方法を備忘録として残そうと思います。 同じように悩んでいる方がいたら参考にしてみてください。 アプリ概要 ログインユーザーが他の登録ユーザーを選択してチャットを開始するという、DMのようなものが主機能のアプリです。 リレーション usersテーブルとroomsテーブルが多対多の関係性のため、N+1問題が懸念されるので、中間テーブルとしてroom_usersテーブルを作成しました。 環境 Spring2.5.5 gradle MyBatis2.2.0 MySQL 中間テーブルへのinsertを実装 今回の実装では、 ①チャットルーム作成時に、roomsテーブルにチャットルーム情報を登録する ②作成者のユーザーIDと作成者が選択したユーザーのユーザーID、作成したチャットルームのIDを中間テーブルに登録する 上記2つの処理が必要です。 ということで実装内容を解説します。 entity MUser.java @Data public class MUser { private int id; private String name; private String email; private String password; private String passwordConfirmation; private String role; @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss") private LocalDateTime createdAt; @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss") private LocalDateTime updatedAt; } MRoom.java @Data public class MRoom { private int id; private String roomName; @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss") private LocalDateTime createdAt; } TRoomUser.java @Data public class TRoomUser { private int id; private int roomId; private int currentUserId; private int userId; @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss") private LocalDateTime createdAt; } entityクラスのポイントは中間テーブルのTRoomUserクラスです。 roomsテーブルのIDにroomId、チャットルーム作成者のIDをcurrentUserIdに、選択されたユーザーのIDをuserIdとして定義しています。 RoomMapper RoomMapper.java @Mapper public interface RoomMapper { /**チャットルーム登録*/ public int insertOneRoom(MRoom room); } RoomMapper.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- Mapperとxmlのマッピング --> <mapper namespace="com.example.demo.repository.RoomMapper"> <!-- マッピング定義(rooms) --> <resultMap type="com.example.demo.entity.MRoom" id="room"> <id column="id" property="id"></id> <result column="room_name" property="roomName"></result> <result column="created_at" property="createdAt"></result> <collection property="roomUserList" resultMap="roomUser"></collection> </resultMap> <!-- チャットルーム登録 --> <insert id="insertOneRoom"> insert into rooms ( id, room_name, created_at ) values ( #{id,jdbcType=INTEGER}, #{roomName,jdbcType=VARCHAR}, #{createdAt,jdbcType=TIMESTAMP} ) <selectKey resultType="int" keyProperty="id" order="AFTER"> select @@IDENTITY </selectKey> </insert> </mapper> まず①の処理ですが、roomsテーブルへの登録なので単純にinsert文を作成するだけで大丈夫です。 また、selectKeyタグで主キーであるidを取得していますが、後ほど使用するために取得しています。 RoomUserMapper RoomUserMapper.java @Mapper public interface RoomUserMapper { public int insertRoomUser(TRoomUser roomUser); } RoomUserMapper.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- Mapperとxmlのマッピング --> <mapper namespace="com.example.demo.repository.RoomUserMapper"> <!-- マッピング定義(room_user) --> <resultMap type="com.example.demo.entity.TRoomUser" id="roomUser"> <id column="id" property="id"></id> <result column="room_id" property="roomId"></result> <result column="current_user_id" property="currentUserId"></result> <result column="created_at" property="createdAt"></result> <result column="user_id" property="userId"></result> </resultMap> <!-- room_user登録 --> <insert id="insertRoomUser"> insert into room_users ( id, room_id, current_user_id, created_at, user_id ) values ( #{id,jdbcType=INTEGER}, #{roomId,jdbcType=INTEGER}, #{currentUserId,jdbcType=INTEGER}, #{createdAt,jdbcType=TIMESTAMP}, #{userId,jdbcType=INTEGER} ) </insert> </mapper> 中間テーブルもMapperに関してはroomsテーブルと同様、登録の処理を記述するだけでOKです。 RoomService RoomService.java public interface RoomService { /**チャットルーム登録*/ public void insertRoom(MRoom room, RoomForm form); } RoomServiceImpl.java @Service public class RoomServiceImpl implements RoomService { @Autowired private RoomMapper mapper; /** *チャットルーム登録 */ @Transactional @Override public void insertRoom(MRoom room, RoomForm form) { //チャットルーム名取得 room.setRoomName(form.getRoomName()); //現在時刻の取得 LocalDateTime now = LocalDateTime.now(); room.setCreatedAt(now); //チャットルーム登録 mapper.insertOneRoom(room); } } Serviceクラスは一つにまとめることもできますが、Mapperと同じメソッド名になるとわかりにくくなるため、自分の場合は、インターフェースと実装クラスで分けています。 ロジックですが、チャットルーム名はフォームに入力された値を取得して、それをセットするだけです。 作成日時も現在時刻を取得し、それをセットするだけで簡単に実装できます。 最後にroomsテーブルに登録するためRoomMapperインターフェースの登録メソッドを呼び出して登録処理を行います。 Form RoomForm.java @Data public class RoomForm { @NotBlank private String roomName; private int userId; } チャットルーム登録画面のフォームクラスです。 自分の実装はプルダウンから、選択したユーザーとチャットができる仕様のため、プルダウンに入力されるユーザーの情報を取得するためuserIdを定義しています。 RoomUserService RoomUserService public interface RoomUserService { /**room_user登録*/ public void registRoomUser(RoomForm form, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser); } RoomUserServiceImpl.java @Service public class RoomUserServiceImpl implements RoomUserService { @Autowired private RoomUserMapper mapper; @Autowired private RoomService service; @Transactional @Override public void registRoomUser(RoomForm form, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser) { //formをMRoomクラスに変換 MRoom room = new MRoom(); //チャットルーム登録 service.insertRoom(room, form); //ログインユーザーのユーザーID取得 int currentUserId = loginUser.getUser().getId(); //roomsテーブルのIDを設定(FK) roomUser.setRoomId(room.getId()); //ログインユーザーのIDを設定 roomUser.setCurrentUserId(currentUserId); //プルダウン選択されたユーザーIDを設定 roomUser.setUserId(form.getUserId()); //現在時刻の取得 LocalDateTime now = LocalDateTime.now(); roomUser.setCreatedAt(now); //roomUserTBL登録 mapper.insertRoomUser(roomUser); } } 少し処理が多めですが、中間テーブルへの登録ロジックです。 ポイントは、このロジックの中でRoomServiceインターフェースのinsertRoom()メソッドを呼び出している点です。 これにより、roomsテーブルの登録と同時にroom_usersテーブルの登録も行うことができ、roomsテーブルのidを取得して、room_usersテーブルのroomIdカラムに値を設定できます。 また、roomsテーブルに登録するメソッド(insertRoom())の前にroomsテーブルのエンティティのインスタンス(MRoom room = new MRoom();)を作成しておくことも重要です。 これがないと、チャットルーム登録処理時に引数としてMRoomのエンティティを渡せないため登録処理自体が行えないため、最初にインスタンスを作成することが必要です。 その後、UserDetailServiceImpllクラスで取得したログインユーザーのIDを取得、フォームから送られるユーザーIDを取得して、各自セッターで中間テーブルへ登録する値を設定します。 最後に、中間テーブルのMapperに定義している登録メソッドを呼び出せば、roomsテーブルの登録と同時に、中間テーブルへも値を登録できるというロジックが完成します。 なお、ログインユーザーの取得については、以下の記事で詳しく解説しているので、参考にしてみてください。 SpringSecurityとSpringBootでログイン認証と投稿機能を実装する RoomController RoomController @Controller @RequestMapping("/") @Slf4j public class RoomController { @Autowired private UserService userService; @Autowired private RoomService roomService; @Autowired private RoomUserService roomUserService; @GetMapping("/rooms/new") public String getRoomsNew(Model model, @ModelAttribute("form") RoomForm form, @AuthenticationPrincipal UserDetailServiceImpll loginUser) { //ログインユーザーのユーザーID取得 int currentUserId = loginUser.getUser().getId(); //ユーザー取得(複数件) List<MUser> users = userService.getUsers(currentUserId); model.addAttribute("users", users); return "rooms/new"; } @PostMapping("/rooms/new") public String postRoomsNew(Model model, @Validated @ModelAttribute("form") RoomForm form, BindingResult result, TRoomUser roomUser, @AuthenticationPrincipal UserDetailServiceImpll loginUser) { //入力チェック if(result.hasErrors()) { /* NG:チャットルーム作成画面に戻る*/ return "redirect:/rooms/new"; } log.info(form.toString()); //チャットルーム・roomUserTBL登録 roomUserService.registRoomUser(form, roomUser, loginUser); return "redirect:/"; } } コントローラーの処理ですが、postRoomsNew()メソッドから解説します。 ここでは単純にバリデーションのチェックと、ロジックの呼び出しを行うだけです。 次に、getRoomsNew()メソッドの解説ですが、UserServiceのgetUsers()メソッド`がプルダウンを実装する上で重要になってくるので、解説します。 先に結論から言うと、このメソッドの処理はログインユーザー以外のユーザーを取得するメソッドです。 UserMapper UserMapper.java @Mapper public interface UserMapper { /**ログインユーザー以外のユーザー取得(複数件)*/ public List<MUser> findMany(int id); } UserMapper.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- Mapperとxmlのマッピング --> <mapper namespace="com.example.demo.repository.UserMapper"> <!-- マッピング定義(ユーザー) --> <resultMap type="com.example.demo.entity.MUser" id="user"> <id column="id" property="id"></id> <result column="email" property="email"></result> <result column="password" property="password"></result> <result column="password_confirmation" property="passwordConfirmation"></result> <result column="name" property="name"></result> <result column="role" property="role"></result> <result column="created_at" property="createdAt"></result> <result column="updated_at" property="updatedAt"></result> </resultMap> <!-- ログインユーザー以外のユーザー取得(複数件) --> <select id="findMany" resultType="MUser"> select * from users where not id = #{id} </select> </mapper> ポイントはxmlファイルのSQLです。 WHERE NOT句で条件を指定することで、引数に指定されるID以外の値を取得することができます。 UserService UserService.java public interface UserService { /**ログインユーザー以外のユーザー取得(複数件)*/ public List<MUser> getUsers(int id); } UserServiceImpl.java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper mapper; /**ログインユーザー以外のユーザー取得(複数件)*/ @Override public List<MUser> getUsers(int id) { return mapper.findMany(id); } } View new.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>ChatApp</title> <link rel="stylesheet" th:href="@{/css/rooms/room.css}"> </head> <body> <div class='chat-room-form'> <h1>新規チャットルーム</h1> <form th:action="@{/rooms/new}" method="post" th:object="${form}"> <div class='chat-room-form__field'> <div class='chat-room-form__field--left'> <label for="roomName" th:text="#{roomName}" class="chat-room-form__label"></label> </div> <div class='chat-room-form__field--right'> <input type="text" th:field="*{roomName}" th:errorclass="is-invalid" class="chat__room_name chat-room-form__input" placeholder="チャットルーム名を入力してください"> </div> <div class="invalid-feedback" th:errors="*{roomName}"></div> </div> <div class='chat-room-form__field'></div> <div class='chat-room-form__field'> <div class='chat-room-form__field--left'> <label class='chat-room-form__label' for='chat_room_チャットメンバー'>チャットメンバー</label> </div> <div class='chat-room-form__field--right'> <select id="userId" name="userId"> <option value="">チャットするユーザーを選択してください</option> <option th:each="user: ${users}" th:value="${user.id}" th:text="${user.name}"></option> </select> </div> </div> <div class='chat-room-form__field'> <div class='chat-room-form__field--left'></div> <div class='chat-room-form__field--right'> <input type="submit" name="commit" class="chat-room-form__action-btn"> </div> </div> </form> </div> </body> </html> ポイントはプルダウンのselectタグの部分です。 通常のフォームタグ内ならth:field="*{userId}"としますが、それだとエラーになるため、selectタグの場合はid属性とname属性に入力させたい(DBに送りたい)値を設定します。 プルダウンの初期値を設定する方法は色々ありますが、自分の場合は、シンプルにoptionタグを二つ作り、一つ目のvalue属性の値を空にして設定しています。 二つ目のoptionタグでは、送信したい値をth:value属性に指定し、表示させたい内容をth:name属性に指定します。 先ほど、コントローラーで呼び出したgetUsers()メソッドはList型のため、th:each属性で値を一つずつ表示・取得できるようにしています。 これで、中間テーブル+αの実装は完了です。 自分の場合はこのロジックを考え出すのに半日くらいかかり、実装するのに3時間くらいかかりました。 まだまだJavaであったりSpringBootの文献は非常に少ないので、同志がいたら参考になれば幸いかと思います。
- 投稿日:2021-12-24T12:54:44+09:00
Javaを使用してExcelをPDFに変換する方法
Javaを使用してExcelをPDFに変換する方法 この記事では、ExcelワークブックをJavaプログラムでPDFドキュメントに変換する方法を紹介します。今回は以下の二つの変換について詳細に説明します。 1. Excelワークブック全体をPDFに変換する 2. 指定されたExcelワークシートをPDFに変換する 使用ツール:Free Spire.XLS for Java(無料版) Jarファイルのダウンロードとインポート 公式ウェブサイトからダウンロードします。ダウンロード後、ファイルを解凍し、libフォルダー内のSpire.Xls.jarファイルをJavaプログラムにインポートします。結果は以下のようになります。 Excelのテストドキュメントは次のとおりで、2つのワークシートが含まれています。 【例の1】Excelワークブック全体をPDFに変換する コード一覧 import com.spire.xls.*; public class ExcelToPDF { public static void main(String[] args) { //Excelドキュメントを作成する Workbook wb = new Workbook(); wb.loadFromFile("C:\\Users\\Administrator\\Desktop\\test.xlsx"); //呼び出し元のメソッドをPDF形式で保存する wb.saveToFile("ToPDF.pdf",FileFormat.PDF); } } 変換した結果: 【例の2】指定されたExcelワークシートをPDFに変換する コード一覧 import com.spire.xls.*; public class ExcelToPDF { public static void main(String[] args) { //Excelドキュメントをロードする Workbook wb = new Workbook(); wb.loadFromFile("C:\\Users\\Administrator\\Desktop\\test.xlsx"); //2番目のワークシートを取得する Worksheet sheet = wb.getWorksheets().get(1); //呼び出し元のメソッドをPDF形式で保存する sheet.saveToPdf("ToPDF2.pdf"); } } 変換した結果: 今回のExcelをPDFに変換する記事は以上です、最後まで読んでいただきありがとうございます、もしよかったら他の内容の記事も参照してください。
- 投稿日:2021-12-24T12:54:08+09:00
Elasticsearch + Cloud Run で検索 API を作る
はじめに この記事は ZOZO Advent Calendar 2021 24日目の記事です。 この記事では全文検索エンジンの一つである Elasticsearch を Cloud Run にデプロイして検索APIを作ります。 メルカリさんのテックブログを見たことがある方にはわかるかと思いますが、以下の記事に大きく影響を受けています。 上記記事では Apache Solr を使っています。Solr も Elasticsearch も広く利用されている検索エンジンであり、どちらが優れているということもありませんが、両者の違いが気になる方は以下の記事などが参考になるかと思います。 以降、Elasticsearch を "ES" と省略して表記しています。 システム構成 インデックスを作成する search-indexer は Cloud Run で稼働 生成されたインデックスファイルは GCS に保管 ユーザーからのリクエストを受ける search-api も Cloud Run で稼働 実際に検索を行う es-instance も Cloud Run で稼働 完全に Cloud Run 頼みですね。 ポイントはユーザーのリクエストを受ける search-api は最小インスタンス数(min_instances)を0にして、検索を行う es-instance の最小インスタンス数は1としている部分です。 Cloud Run ではインスタンスに対するトラフィックがない場合に起動させておくインスタンス数を決めておくことができ、それを min_instances というパラメータで制御します。 料金を極力安く抑えるのであれば全て min_instances=0 と設定すれば良いのですが、 ES には登録されたインデックスを保持しておく必要があり、インスタンスが停止すると登録されたインデックスも初期化されてしまうため、 ES のインスタンスは常時1つのインスタンスが起動し続けるような設定としています。 この記事では上記システム構成のうち、 es-instance を Cloud Run にデプロイするところまでを取り上げます。 Elasticsearch をデプロイする Cloud Run にデプロイする前にまずはローカル環境で動くものを作ります。 手っ取り早くローカルで動かすなら以下から ES のバイナリをダウンロードして実行する方法が良いと思います。 ESを起動 $ ./bin/elasticsearch が、今回は最終的に Cloud Run にデプロイするため Docker で動かします。 ローカルでの起動 イメージのビルド Dockerfile を作成します。日本語を使うためにプラグインを追加しています。 Dockerfile FROM docker.elastic.co/elasticsearch/elasticsearch:7.15.1 RUN elasticsearch-plugin install analysis-kuromoji RUN elasticsearch-plugin install analysis-icu このイメージをビルドします。 $ docker build -t es-run . 動作確認 Elasticsearch を立ち上げ、ヘルスチェックをしてみます。 $ docker run -it --rm -p 9200:9200 es-run # ヘルスチェック $ curl 'http://localhost:9200/' { "name" : "es01", "cluster_name" : "elasticsearch", "cluster_uuid" : "wGcmJOWPQlGjYr-7xpK2lw", "version" : { "number" : "7.15.1", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "83c34f456ae29d60e94d886e455e6a3409bba9ed", "build_date" : "2021-10-07T21:56:19.031608185Z", "build_snapshot" : false, "lucene_version" : "8.9.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } Cloud Run へのデプロイ 注意事項 あとは Cloud Run へデプロイするだけですが、デプロイするにあたっていくつか注意点があります。 まずポイントとなるのは、クラスタをシングルノードで立ち上げることです。 docker-compose を使うと kibana や他のノードへの接続が容易にできますが、Cloud Run で起動できるのは一つのコンテナなのでシングルノードで立ち上げることになります。 シングルノードで立ち上げた場合、ノードが落ちると即サービス停止となるため可用性は低く、本番運用に向いていません。 今回紹介する Elasticsearch + Cloud Run の構成はそうした制約があることを念頭におく必要があります。 各種 ES の設定 シングルノードとなるように ES の設定を行います。自分はここの設定で一番詰まりましたが、以下を見ると結局シンプルな設定で十分ということがわかるかと思います。 ディレクトリ 構成 tree . . ├── Dockerfile ├── cloudbuild.yml ├── config │ ├── elasticsearch.yml │ └── jvm.options └── security └── limits.conf config/elasticsearch.yaml node.name: es01 bootstrap.memory_lock: true network.host: 0.0.0.0 discovery.type: single-node config/jvm.options -Xms512m -Xmx512m security/limits.conf elasticsearch soft nofile 65536 elasticsearch hard nofile 65536 elasticsearch memlock unlimited イメージのビルド・デプロイ 先ほどローカルでビルドしたイメージをそのままデプロイしても良いですが、ここではビルドからデプロイまで全てを Cloud Build で行います。 ポイントは ポート番号を 9200 に指定すること min-instancesを1にすること memory に余裕を持たせておくこと といったところでしょうか。ちなみに _GITHUB_SHA という環境変数がいるのはこの cloudbuild.yaml を CI/CD で利用できるようにしたことの名残です。 cloudbuild.yaml steps: - name: 'gcr.io/cloud-builders/docker' id: Build:Image args: ['build', '-t', '$_DOCKER_URI:$_GITHUB_SHA', '.'] - name: 'gcr.io/cloud-builders/docker' id: Ship:Push args: ['push', '$_DOCKER_URI:$_GITHUB_SHA'] - name: 'gcr.io/cloud-builders/gcloud' id: Ship:Deploy args: ['run', 'deploy', 'es-run', '--image', '$_DOCKER_URI:$_GITHUB_SHA', '--region', 'asia-northeast1', '--platform', 'managed', '--min-instances', '1', '--cpu', '4', '--memory', '4Gi', '--port', '9200', '--no-allow-unauthenticated' ] timeout: 3600s images: - $_DOCKER_URI:$_GITHUB_SHA options: machineType: 'E2_HIGHCPU_8' Cloud Build で Cloud Run にデプロイするために以下の権限をサービスアカウントまたは自分自身のユーザーアカウントに付与します。 roles/run.developer roles/cloudbuild.builds.editor 一番手っ取り早いのは roles/editor を付与してしまうことですね。 続いて Cloud Build を実行します。 $ export PROJECT_ID=<your-project> $ gcloud builds submit --substitutions=_DOCKER_URI=gcr.io/${PROJECT_ID}/es-run,_GITHUB_SHA=latest うまくデプロイされると以下のようなログが出力されます。 ID: 5f04ad8a-7724-4a38-a579-78d2ec43a48f CREATE_TIME: 2021-12-22T03:19:28+00:00 DURATION: 2M26S SOURCE: gs://<PROJECT_ID>_cloudbuild/source/1640315967.398871-209c689985db46cc93b2c6c8c030cffa.tgz IMAGES: gcr.io/<PROJECT_ID>/es-run (+1 more) STATUS: SUCCESS 動作確認 Cloud Run のエンドポイントを叩くため、今回はヘッダーに Bearer トークンを付与します。 この Bearer トークンはユーザー自身の権限に紐づくトークンなので、予め roles/run.invoker の権限を付与しておきましょう。 $ export RUN_URL=<Cloud Run のエンドポイント> $ curl -s \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $(gcloud auth print-identity-token)" \ ${RUN_URL} { "name" : "es01", "cluster_name" : "elasticsearch", "cluster_uuid" : "I1xcMEiRQECoQ9iMeqEfYQ", "version" : { "number" : "7.15.1", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "83c34f456ae29d60e94d886e455e6a3409bba9ed", "build_date" : "2021-10-07T21:56:19.031608185Z", "build_snapshot" : false, "lucene_version" : "8.9.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } ここまで来れば、あとはインデックス用のファイルを作って登録して検索APIとして遊ぶことができます。 注意事項として、上記の手順で作成された Cloud Run のサービスは min_instances=1 で作成しているのでインスタンスの消し忘れに注意が必要です。 デフォルトではmin_instances=0となっているためトラフィックが来ないと自動的にコンテナが停止しますが、上記の設定ではコンテナは停止せず課金され続けます。 GCP に限らず、クラウドサービスで遊んだ後のお片付けは忘れないようにしましょう。 # min_instances を更新する場合 $ gcloud run services update es-run --min-instances=0 # Cloud Run のサービスを削除する場合 $ gcloud run services delete es-run まとめ 本記事では Elasticsearch と Cloud Run を組み合わせて簡易検索 API を作成しました。 ただAPIとは名ばかりで、 ES を Cloud Run にデプロイしただけなのでインターフェースとなる API も必要となります。 そのインターフェースにあたるものが記事冒頭のシステム構成に記載した search-api ですが、時間の都合上省略してしまったのでまた別な記事でご紹介できればと思います。 明日の記事はいよいよアドベントカレンダー最終日、担当は ZOZO のメシア、我らがそのっつさんです。
- 投稿日:2021-12-24T09:01:09+09:00
Effective Java[第3版] 学習のまとめ
[項目1]コンストラクタの代わりにstaticファクトリメソッドを検討する クラスのインスタンスを提供するにはコンストラクタを使用するのが一般的と考えられているかが、その他にstaticファクトリメソッドというものがある。 それぞれにメリットデメリットがあるため、場合によってどちらを使うかを検討する必要があり、無意識にコンストラクタを使用することは避けるべきである。 staticファクトリメソッドとは クラスのインスタンスを返す単なるstaticメソッド public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; } staticファクトリメソッドの長所 1.名前を持つことができる 例えば確率的素数(probable prime) のBigIntegerを返す場合 コンストラクタ BigInteger(int, int, Random) staticファクトリメソッド BigInteger.probablePrime(int, Random) staticファクトリメソッドの方が、ぱっと見で何をするのかわかりやすい。 コンストラクタではクラス名と同じ名前に限定される。 引数違いによってコンストラクタを区別するためわかりずらい。 2.オブジェクトを再利用可能 何度staticファクトリメソッド呼び出しても同じオブジェクトを返すことができるので、インスタンスを制御することができる。 インスタンスを制御できることでクラスをシングルトンにできる、インスタンス化不可能にできるなどのメリットもある。 (詳しくは項目3,4) 3.返す型を柔軟に選べる 4.引数によって、返すオブジェクトの型を自由に決められる enumが64個以下で構成されている場合は、RegularEnumSetInstanceを返し、64個を超える場合は、JumboEnumSetを返す。 パフォーマンスを考えてこのような実装にしているが、利用者は内部の実装を意識することなく利用できる。 5.返されるオブジェクトのクラスは実行時に決まってもよい staticファクトリメソッドの短所 1.サブクラスを作れない staticなメソッドのみを提供する場合、サブクラスを作ることができない。 2.staticファクトリメソッドであるかがわかりづらい コンストラクタと比べて、JavaDocなどのドキュメンテーションが目立たないため、インスタンス化するを利用者が知ることが困難となる。 一般的な命名規則に沿って名前をつけるよう心掛ける。 [項目2] 多くのコンストラクタパラメータに直面したときはビルダーを検討する 項目1でのstaticファクトリメソッドとコンストラクタはどちらも、多くのパラメータを持つ場合上手く対応することができない課題がある。 このような場合Builderパターンを使用するのはいい選択である。 利用者がインスタンスを生成する時、Builderパターンはテレスコーピング・コンストラクタ・パターンより読みやすく、JavaBeanパターンよりも安全である。 テレスコーピング・コンストラクタ・パターン メリット 不変オブジェクトにできる デメリット 必須でないパラメータに対しても、利用者はパラメータを渡す必要がある 利用者はパラメータの順番を気にする必要があり、読みづらい 型があっていればエラーにならないため、不具合に気付かない JavaBeanパターン メリット インスタンス化するクラスのフィールドを初期化することで、必須のパラメータのみ利用者に強制することができる 読みやすい。パラメータの値を把握しやすい デメリット インスタンス化した後に、パラメータ間で不整合が起こる可能性があり、その場合デバッグが困難 setterがメソッドがあるため不変オブジェクトにできない Builderパターン 上記の2パターンのデメリットを克服できる メリット 読みやすい。パラメータの値を把握しやすい 不変オブジェクトにできる インスタンス化する前に、パラメータの不整合を検知できるので、デバッグが簡単 [項目3]privateのコンストラクタかenum型でシングルトンを特性を強制する シングルトンは一度しか院セタンスを作成しないクラスである。 シングルトンを実装する方法は3種類ある。 //public staticのフィールド public class Elvis{ public static final Elvis INSTANCE = new Elvis(); private Elvis(){} } //staticファクトリ public class Elvis{ private static final Elvis INSTANCE = new Elvis(); private Elvis(){} public static Elvis getInstance(){ return INSTANCE; } } //enum public enum Elvis { INSTANCE; public void someMethod(); } public staticフィールド、staticファクトリの手法ではシングルトンでなくなリスクがあるため、回避する手間が必要となる。 enum型はその手間が必要なく、簡潔で分かりやすため、シングルトンを実装する場合はたいていが最善の方法となる。 [項目4]privateのコンストラクタでインスタンス化不可能を強制する staticのメソッドとstaticのフィールド構成されるクラス(ユーティリティクラス)はインスタンス化されるように設定されていない。 明示的なコンストラクタが存在しない場合、デフォルトコンストラクタを提供する。 ユーザーからはこのコンストラクタは区別ができず、本来の意図に反してインスタンス化してしまうこと考えられる。 このようなことがないようにprivateなコンストラクタを実装することで、外部からインスタンス化を防ぐ。 public class UtilityClass { private UtilityClass() { throw new AssertionError();//なくてもよいが、保険 } } [項目5]資源を直接結び付けるよりも依存性注入を選ぶ 多くのクラスが仮想の資源に依存している。例えばスペルチェッカーは辞書に依存している。 以下2つの実装は、辞書が1つかないと仮定している。様々な種類の辞書についてテストできない。 下層の資源でパラメータ化rされた振る舞いを持つクラスには不適切である。 //静的なユーティリティの不適切な使用 public class SpellChecker{ private static final Lexicon dictionary = ...; private SpellChecker() {} //インスタンス化できない(項目4) public static boolean isValid(String word) { ... } } //シングルトンの不適切な使用 public class SpellChecker{ private final Lexicon dictionary = ...; private SpellChecker() {} public static SpellChecker INSTANCE = new SpellChecker(...); public static boolean isValid(String word) { ... } } 必要なことは複数のインスタンスをサポートしてそれぞれのインスタンスで、資源を使えること。 そのため新しいインスタンスを生成するときにコンストラクタに資源を渡す。これを依存性注入と言う。 依存性注入は柔軟性とテスト可能性を大幅に向上させる。 public class SpellChecker{ private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public static boolean isValid(String word) { ... } } [項目6]不必要なオブジェクトの生成を避ける 機能的に同じオブジェクトを必要とする場合、そのたびに新しいオブジェクトを生成するより、再利用することが望ましい。 オブジェクトが不変(immutable)である場合は常に再利用することができる。 //sample1 //NG String s = new String("bikini"); //OK String s = "bikini"; //sample2 //NG new Boolean(String); //OK Boolean.valueOf(String); //sample3 //NG //isRomanNumeral()が呼び出されるたび、matchesの内部でPatternオブジェクトが生成される。 static boolean isRomanNumeral(String s){ return s.matches("hogehoge"); } //OK private static final Pattern ROMAN = Pattern.compile("hogehoge"); static boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); } //sample4 //NG //sumに足されるたびにオートボクシングされる private static long sum() { Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) // Longはイミュータブルなので、加算の度に新たなインスタンスが生成されてしまう。 sum += i; return sum; } //OK private static long sum() { // プリミティブ型に変更(Long -> long) long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i; return sum; } [項目7]使われなくなったオブジェクト参照を取り除く Javaではガベージコレククション機能により、オブジェクトを使い終えた時、占有していたメモリ領域を自動的に開放し、空き領域として再利用することができる。 ただメモリ管理を考える必要がないというわけではない。 スタックの実装を考えると、Pushをすれば配列に要素が入り、Popをすれば配列から最後に入れた要素が取得する動作となる。 Popする際は参照する番号をずらしているだけなので、オブジェクトの参照自体は残り続ける。参照が残っているためガベージコレクタは削除対象か判断できない。 メモリ不足を発生させないように、Popする際にnullを入れて参照をはずす処理が必要となる。 [項目9]try-finallyよりもtry-with-resourcesを選ぶ try-finally 句でcloseメソッド処理を実装すると、以下の問題が考えられる。 クローズ忘れ ネストが深くなり読みづらい 例外を握りつぶしやすい try-with-resourcesを使うとこの問題が解消されるので、選択するようにする。 //try-finally static void copy(String src, String dest) throws IOException { InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dest); try { byte[] buf = new byte[100]; int n; while ((n = in.read(buf)) >= 0) { out.write(buf, 0, n); } } finally { out.close(); } } finally { in.close(); } } //try-with-resources static void copy(String src, String dest) throws IOException { try (InputStream inputStream = new FileInputStream(src); OutputStream outputStream = new FileOutputStream(dest)) { byte[] buf = new byte[100]; int n; while ((n = in.read(buf)) >= 0) { out.write(buf, 0, n); } } } [項目10]equalsをオーバーライドするときは一般契約に従う equalsをオーバーライドすることは簡単であるが、間違ったやり方で実装してしまうこともある。実装不備を起こさない最も簡単な方法はオーバーライドしないことである。 以下の条件に該当する場合はオーバーライドしないほうがいい。 クラスの個々のインタタンスが、本質的に一意である 論理的等価性を検証する必要がない スーパークラスが既にequalsをオーバーライドしており、スーパークラスの振る舞いがこのクラスに対して適切である クラスがprivateあるいはパッケージプライベートであり、そのequalsメソッドが決して呼び出されないことが確かである 上記に該当しない場合にオーバーライドすることが適切である。これは値クラス(value class)である。 もしequalsをオーバーライドする場合は以下の一般契約を守る必要がある。 反射的(reflexive):nullでない任意の参照値xに対して、x.equals(x)はtrueを返さなければならない 対照的(symmetric):nullでない任意の参照値xとyに対して、y.equals(x)がtrueを返す場合にのみ、x.equals(y)はtrueを返さなければならない 推移的(transitive):nullでない任意の参照値x,y,zに対して、もしx.equals(y)とy.equals(z)がtrueを返すならば、x.equals(z)はtrueを返さなければならない 整合的(consistent):nullでない任意の参照値xとyに対して、オブジェクトに対するequals比較に使用される情報が変更されなければ、x.equals(y)の複数回呼び出しは、終始一貫してtrueを返すか、終始一貫してfalseを返さなけらばならない 一般契約についてはサンプルコードが書籍には載っているがここでは割愛する。実装する際はIDEの機能で自動作成できるので、リファクタリング、実装に手を加える時は一般契約を意識する [項目11]equalsをオーバーライドするときは、常にhashCodeをオーバーライドする equalsをオーバーライドするクラスでは必ずhashCodeをオーバーライドする必要がある。 そうしなければObjects.hashCodeの一般契約を破ることになってしまう。 一般契約とは・・・ hashCodeメソッドが複数回は呼ばれた場合に同じ値を返す 2つのオブジェクトがequalsメソッドで等しい場合、2つのオブジェクトのhashCodeメソッドは同じ値を返す 2つのオブジェクトがequalsメソッドで等しくない場合、2つのオブジェクトのhashCodeメソッドは別の値を返す必要はない オーバーライドをしないと上記の2番目の項目を破ってしまう。 hashCodeを実装 IDEの機能で自動作成 ライブラリを使う(Lombok) [項目12]toStringを常にオーバーライドする toStringをオーバーライドすることで、クラスの利用者がデバッグしやすくなる。 返却される文字列は簡潔だが、人が読みやすくなっている説明的な表現であることが望ましい。 オブジェクトに含まれる情報(フィールド)をすべて表示させる。 equalsやhashCodeと同様にIDEの機能で保管してくれる。 equalsやhashCodeほど必須とはいえないが、インスタンス化可能なクラスでは、スーパークラスがオーバーライドしていないなら、実装することが望ましい public class Student { int studentNo; String name; int age; public Student(int studentNo, String name, int age) { this.studentNo = studentNo; this.name = name; this.age = age; } @Override public String toString() { return “Student [studentNo=” + studentNo + “, name=” + name + “, age=” + age + “]”; } } public class App { public static void main(String[] args) { Student student = new Student(1, “tanaka taro”, 12); System.out.println(student); //StudentクラスでtoStringをオーバーライドしない場合 // → item12.Student@515f550a //StudentクラスでtoStringをオーバーライドする場合 // → Student [studentNo=1, name=tanaka taro, age=12] } } [項目15]クラスやメンバーのアクセス可能性を最小限にする うまく実装されているコンポーネントは、その実装のすべてを隠蔽し、実装とAPIを分離している。コンポーネントはAPIを通してのみ、他のAPIと通信し、お互いに内部の動作は知らない。この概念を情報隠蔽、カプセル化という。 情報隠蔽、カプセル化をすることにより、コンポーネントを個別に開発可能で、他のコンポーネントへのリスクを考えずに実装することができる。 情報隠蔽、カプセル化のメリット 各コンポーネントを並列して開発できるようにして、開発のスピードを速める 影響調査の手間を減らして、保守の負荷をさげる ボトルネックを局所的に解消できるようにして、パフォーマンスチューニングがしやすい アクセス修飾子を使用して、クラス、インターフェース、メンバーのアクセス可能性を制御する アクセス修飾子の種類 種類 概要 private メンバーは宣言されたクラス内のみアクセス可能 パーケージプライベート メンバーは宣言された、パッケージ内からどこからでもアクセス可能 protected メンバーはそのクラストサブクラスからアクセス可能 public メンバーはどこからでもアクセス可能 常にプログラム要素のアクセス可能性を最小限にするべきであるが、これにあてはまらない場合もある メソッドがスーパークラスのメソッドはオーバーライドする場合、スーパークラスのメソッドが持っているより低いアクセス修飾子を持つことはできない。サブクラスをコンパイルする場合エラーとなる テスト容易性のため緩和する。publicクラスのpraveteメソッドパッケージプライベートにするのはOK。 [項目16]publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う クラスのフィールドは基本的にpublicとせずに、privateとする。 publicだと直接アクセスできるでのカプセル化の恩恵を提供できない。外部から値を変更される危険性もあり不変式の強制ができない。 解決策としてはフィールドをprivateとし、publicのアクセッサーメソッド(getter),ミューテーターメソッド(setter)を作成する。 ただし、不変オブジェクトとする場合はgetterのみ作成する。 getter,setterはIDEの機能で自動作成でき、getter,setterどちらかのみを作成することも選択できる。 [項目18]継承よりもコンポジションを選ぶ 継承はコードを再利用するには便利な機能ですが、常に既存のメソッドを継承し、拡張することがベストな方法とは限らない。 サブクラスとスーパークラスが同じ開発者の管理下にあり、同じパッケージ内にある場合は比較的安全である。 しかしパッケージをまたがり具象クラスを継承すると、意図しない動作を起こす可能性もある。 書籍では不適切な継承の例として、HashSetを継承したInstrumentedHashSetクラスでadd,addAllをオーバーライドしている。 InstrumentedHashSetに要素を追加すると、要素の追加された回数がカウントアップされるが、addAllを呼び出すと、意図する結果の2倍の数が返される。これはaddAllの中でaddが呼ばれるため意図しない結果となってしまう。 コンポジション このような事象を解決する手段にコンポジションと呼ばれるものがある。 既存クラスを拡張するのではなく、新たなクラスに既存のクラスのインスタンスをprivateフィールドで持たせ、既存クラスが新たなクラスの構成要素となる 新たなクラスのメソッドは既存クラスのインスタンスに対して、メソッドを読み出しそのまま結果を返します。これを転送(fowarding)と呼ぶ。 コンポジションを利用すれば、新たなクラスは既存のクラスの実装の詳細に依存することがなくなり、意図する結果が得られる。 では、継承とコンポジションをどのようにつか分けるべきか。 継承はサブクラスがスーパークラスのサブタイプである場合に拡張するべきです。 サブクラスとスパークラスとの間に「is-a」の関係が存在しているか考え、満たす場合拡張する。 [項目69]例外的状態にだけ例外を使う 言葉の通りであるが、例外は例外的状態で使われるべきである。通常の制御に対して例外を使わない。 以下の例は他の実装方法があるにもかかわらず、例外を使用しているかなりBadパターン。 //NG int result = 0; try { int i = 0; while (true) { result += digets[i++]; } } catch (ArrayIndexOutOfBoundsException ignored) { } return result; //OK int result = 0; for (int current : digets) { result += current; } return result; [項目19]継承のために設計および文章化する、でなければ継承を禁止する スーパークラスを作成する際はJavadocを書いてドキュメント化しましょうという話。 ただし継承を前提にクラスを設計するのは大変である。クラスの自己利用を全て文書化する必要がある。そして一度文書化したらクラスはその内容を守り続けなければならない。 サブクラスの必要性がないならば、クラスをfinalにするコンストラクタを提供しないなどして継承を禁止するのが良い。 [項目20]抽象クラスよりもインタフェースを選ぶ [項目21]将来のためにインタフェースを設計する インターフェースの設計は慎重に行いましょうという話。変更するにはリスクが伴う。 Java8以降では、インターフェースに新たなメソッドを追加するが可能となりり、デフォルトメソッドを使うことで実装クラスが追加したインターフェースのメソッドを実装しなくてもコンパイルエラーとならなくなったが、実行時にエラーとなる可能性もある。 インターフェースを公開する前にしっかり検証を行うことが大切である。 [項目22]型を定義するためだけにインタフェースを使う 定数を定義するためだけのインターフェースは不適切である。定数だけを定義するのであれば使用するクラスで宣言する、enumやユーティリティクラスを提供するべきである。
- 投稿日:2021-12-24T08:52:03+09:00
AtCoder Beginner Contest 010をやった(Java)
AtCoder Beginner Contest 010をやった。 たぶんC問題解けるからのちに追加予定。 A あだ名をどう求めるかです。 import java.util.*; public class Main { public static void main(String[] args) throws Exception { // Your code here! Scanner scan = new Scanner(System.in); System.out.println(scan.nextLine().concat("pp")); } } B いい方法ないかなーと思ったけど、実践的に考えたら「1 <= 入力値 <= 9」らしいので、パターンで判定した import java.util.*; public class Main { public static void main(String[] args) throws Exception { // Your code here! Scanner scan = new Scanner(System.in); int[] num = new int[scan.nextInt()]; for(int i = 0; i < num.length; i++){ num[i] = scan.nextInt(); } int cnt = 0; for(int i = 0; i < num.length; i++){ if(num[i] == 2 || num[i] == 4 || num[i] == 8){ cnt += 1; } else if(num[i] == 5){ cnt += 2; } else if(num[i] == 6){ cnt += 3; } } System.out.println(cnt); } }
- 投稿日:2021-12-24T00:21:50+09:00
Java ストリーム
Javaのカレンダー | Advent Calendar 2021の15日目の記事です ※ 少し呑んでます 概要 Java の Stream と List(等) で1000件くらいのオブジェクトを扱ったときのヒープ(メモリ)の使用量がどのように変わるかを確認しました 結果は、Stream の方が List よりも 1/4 のヒープで動いてそうです。これだけでも、Stream を使うメリットを感じられるのではないでしょうか? 大量のデータを扱うようなシーンでは、Stream API よりも Spring Batch などの専用のフレームワークを利用すべしとあるので、あくまでも比較的件数が限られているものの用途に向いてそうです 比較したコード コードは適当なオブジェクトを 1000回分生成してリストに詰める(Stream から List に変換)するものです。 Stream を使う場合 void useStream() { IntStream.range(0, 1000).parallel().mapToObj(i -> create()).collect(Collectors.toList()); } parallel() があってこそのこの結果で、parallel() がないと、 List の場合とほぼ変わらない、もしくはちょっと大きい結果になりました m(_ _)m List を使う場合 void useList() { List<Parson> l = new ArrayList<>(); for (int i = 0; i < 1000; i++) { l.add(create()); } } 測定結果 測定には、以前の記事 Javaでヒープサイズ測定 JUnit5編 で使用した、quickperf を使いました Stream を使う場合 [QUICK PERF] Measured heap allocation (test method thread): 137.34 Kilo bytes (140 640 bytes) List を使う場合 [QUICK PERF] Measured heap allocation (test method thread): 510.02 Kilo bytes (522 264 bytes) 付録 テストコード全体 @QuickPerfTest public class StreamVsListTest { @MeasureHeapAllocation @Test void useStream() { IntStream.range(0, 1000).parallel().mapToObj(i -> create()).collect(Collectors.toList()); } @MeasureHeapAllocation @Test void useList() { List<Parson> l = new ArrayList<>(); for (int i = 0; i < 1000; i++) { l.add(create()); } } @AllArgsConstructor @Data class Parson { private String name; private int age; private String addr; private String addr2; } Parson create() { return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20)); } }