Quando um SQL é enviado para o banco, a base de dados gera um plano de execução. Ou seja, ele compila a query, otimiza, escolhe para ela os melhores índices, carrega as triggers e constraints e deixa o banco “preparado” para executá-la. Por isso o comando chama-se PrepareStatement.
A diferença de um PreparedStatement de um Statement comum, é que justamente o PreparedStatement permite que você faça todo esse setup apenas uma vez, e envie para ele apenas os dados na sequência, dispensando a necessidade do banco refazer todo esse processo de compilação. Isso agiliza muito queries que rodem dentro de fors, porém, como o banco é obrigado a manter o estado dessa compilação ativo, tem (um pouco) menos performance caso você queira rodar a query sozinha (a diferença é tão pequena que geralmente não compensar o risco de ter SQL injection). Isso sem contar o tempo que leva para a query circular pela rede, que no caso de deixa-la dentro do for também será duplicado.
O batchUpdate é outra otimização. Mandar um dado por vez é menos eficiente não por causa do banco, mas do canal de comunicação. Você usa a rede query-por-query, assim, cada transmissão de dados irá sofrer delay do canal, e isso pode deixar o banco ocioso, aguardando queries. No caso do batchUpdate, você monta para o banco um “pacotão”, com vários dados, e então manda uma única grande massa de dados para o BD (muitos protocolos são inclusive capazes de compactar esse pacote, para otimizar banda de rede). O banco então pega o Statement preparado e executa rapidamente para todo o batch, sem ter que ficar esperando nada. Só deve-se cuidar no caso do batchUpdate pois é necessário ter memória para armazenar o batch. Por isso, caso sua query tenha muitos registros, você deve organizar batchs com algum limite, como 200 em 200 registros.